diff options
Diffstat (limited to 'core/java')
423 files changed, 34714 insertions, 15975 deletions
diff --git a/core/java/android/alsa/AlsaCardsParser.java b/core/java/android/alsa/AlsaCardsParser.java index f9af979..8b44881 100644 --- a/core/java/android/alsa/AlsaCardsParser.java +++ b/core/java/android/alsa/AlsaCardsParser.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.alsascan; +package android.alsa; import android.util.Slog; import java.io.BufferedReader; diff --git a/core/java/android/alsa/AlsaDevicesParser.java b/core/java/android/alsa/AlsaDevicesParser.java index 094c8a2..82cc1ae 100644 --- a/core/java/android/alsa/AlsaDevicesParser.java +++ b/core/java/android/alsa/AlsaDevicesParser.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.alsascan; +package android.alsa; import android.util.Slog; import java.io.BufferedReader; diff --git a/core/java/android/alsa/LineTokenizer.java b/core/java/android/alsa/LineTokenizer.java index c138fc5..78c91b5 100644 --- a/core/java/android/alsa/LineTokenizer.java +++ b/core/java/android/alsa/LineTokenizer.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.alsascan; +package android.alsa; /** * @hide diff --git a/core/java/android/animation/BidirectionalTypeConverter.java b/core/java/android/animation/BidirectionalTypeConverter.java new file mode 100644 index 0000000..960650e --- /dev/null +++ b/core/java/android/animation/BidirectionalTypeConverter.java @@ -0,0 +1,73 @@ +/* + * 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; + +/** + * Abstract base class used convert type T to another type V and back again. This + * is necessary when the value types of in animation are different from the property + * type. BidirectionalTypeConverter is needed when only the final value for the + * animation is supplied to animators. + * @see PropertyValuesHolder#setConverter(TypeConverter) + */ +public abstract class BidirectionalTypeConverter<T, V> extends TypeConverter<T, V> { + private BidirectionalTypeConverter mInvertedConverter; + + public BidirectionalTypeConverter(Class<T> fromClass, Class<V> toClass) { + super(fromClass, toClass); + } + + /** + * 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. + * @param value The Object to convert. + * @return A value of type T, converted from <code>value</code>. + */ + public abstract T convertBack(V value); + + /** + * Returns the inverse of this converter, where the from and to classes are reversed. + * The inverted converter uses this convert to call {@link #convertBack(Object)} for + * {@link #convert(Object)} calls and {@link #convert(Object)} for + * {@link #convertBack(Object)} calls. + * @return The inverse of this converter, where the from and to classes are reversed. + */ + public BidirectionalTypeConverter<V, T> invert() { + if (mInvertedConverter == null) { + mInvertedConverter = new InvertedConverter(this); + } + return mInvertedConverter; + } + + private static class InvertedConverter<From, To> extends BidirectionalTypeConverter<From, To> { + private BidirectionalTypeConverter<To, From> mConverter; + + public InvertedConverter(BidirectionalTypeConverter<To, From> converter) { + super(converter.getTargetType(), converter.getSourceType()); + mConverter = converter; + } + + @Override + public From convertBack(To value) { + return mConverter.convert(value); + } + + @Override + public To convert(From value) { + return mConverter.convertBack(value); + } + } +} diff --git a/core/java/android/animation/ObjectAnimator.java b/core/java/android/animation/ObjectAnimator.java index c0ce795..130754e 100644 --- a/core/java/android/animation/ObjectAnimator.java +++ b/core/java/android/animation/ObjectAnimator.java @@ -610,8 +610,8 @@ public final class ObjectAnimator extends ValueAnimator { * 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. + * supplied, the <code>TypeConverter</code> must be a + * {@link android.animation.BidirectionalTypeConverter} to retrieve the current value. * * @param target The object whose property is to be animated. * @param property The property being animated. diff --git a/core/java/android/animation/PropertyValuesHolder.java b/core/java/android/animation/PropertyValuesHolder.java index 8fce80a..bf2924c 100644 --- a/core/java/android/animation/PropertyValuesHolder.java +++ b/core/java/android/animation/PropertyValuesHolder.java @@ -456,7 +456,7 @@ public class PropertyValuesHolder implements Cloneable { * 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 + * must be a {@link android.animation.BidirectionalTypeConverter} to retrieve the current * value. * * @param property The property being animated. Should not be null. @@ -635,6 +635,8 @@ public class PropertyValuesHolder implements Cloneable { /** * Sets the converter to convert from the values type to the setter's parameter type. + * If only one value is supplied, <var>converter</var> must be a + * {@link android.animation.BidirectionalTypeConverter}. * @param converter The converter to use to convert values. */ public void setConverter(TypeConverter converter) { @@ -816,12 +818,12 @@ public class PropertyValuesHolder implements Cloneable { private Object convertBack(Object value) { if (mConverter != null) { - value = mConverter.convertBack(value); - if (value == null) { + if (!(mConverter instanceof BidirectionalTypeConverter)) { throw new IllegalArgumentException("Converter " + mConverter.getClass().getName() - + " must implement convertBack and not return null."); + + " must be a BidirectionalTypeConverter"); } + value = ((BidirectionalTypeConverter) mConverter).convertBack(value); } return value; } diff --git a/core/java/android/animation/TypeConverter.java b/core/java/android/animation/TypeConverter.java index 03b3eb5..9ead2ad 100644 --- a/core/java/android/animation/TypeConverter.java +++ b/core/java/android/animation/TypeConverter.java @@ -53,16 +53,4 @@ public abstract class TypeConverter<T, V> { * @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/app/ActionBar.java b/core/java/android/app/ActionBar.java index 3c3df01..d4c4318 100644 --- a/core/java/android/app/ActionBar.java +++ b/core/java/android/app/ActionBar.java @@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.ActionMode; import android.view.Gravity; +import android.view.KeyEvent; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; @@ -1013,6 +1014,26 @@ public abstract class ActionBar { return null; } + /** @hide */ + public boolean openOptionsMenu() { + return false; + } + + /** @hide */ + public boolean invalidateOptionsMenu() { + return false; + } + + /** @hide */ + public boolean onMenuKeyEvent(KeyEvent event) { + return false; + } + + /** @hide */ + public boolean collapseActionView() { + return false; + } + /** * Listener interface for ActionBar navigation events. * @@ -1291,6 +1312,7 @@ 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) { @@ -1305,6 +1327,7 @@ public abstract class ActionBar { public LayoutParams(LayoutParams source) { super(source); + this.gravity = source.gravity; } public LayoutParams(ViewGroup.LayoutParams source) { diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index ef6fcb7..23b5f29 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.NonNull; +import android.os.PersistableBundle; import android.transition.Scene; import android.transition.TransitionManager; import android.util.ArrayMap; @@ -29,7 +30,6 @@ import com.android.internal.policy.PolicyManager; import android.annotation.IntDef; import android.annotation.Nullable; -import android.app.admin.DevicePolicyManager; import android.content.ComponentCallbacks2; import android.content.ComponentName; import android.content.ContentResolver; @@ -716,6 +716,7 @@ public class Activity extends ContextThemeWrapper HashMap<String, Object> children; ArrayList<Fragment> fragments; ArrayMap<String, LoaderManagerImpl> loaders; + VoiceInteractor voiceInteractor; } /* package */ NonConfigurationInstances mLastNonConfigurationInstances; @@ -777,8 +778,10 @@ public class Activity extends ContextThemeWrapper private Thread mUiThread; final Handler mHandler = new Handler(); - private ActivityOptions mCalledActivityOptions; - private EnterTransitionCoordinator mEnterTransitionCoordinator; + + private ActivityTransitionState mActivityTransitionState = new ActivityTransitionState(); + SharedElementListener mEnterTransitionListener = SharedElementListener.NULL_LISTENER; + SharedElementListener mExitTransitionListener = SharedElementListener.NULL_LISTENER; /** Return the intent that started this activity. */ public Intent getIntent() { @@ -918,10 +921,37 @@ public class Activity extends ContextThemeWrapper } mFragments.dispatchCreate(); getApplication().dispatchActivityCreated(this, savedInstanceState); + if (mVoiceInteractor != null) { + mVoiceInteractor.attachActivity(this); + } mCalled = true; } /** + * Same as {@link #onCreate(android.os.Bundle)} but called for those activities created with + * the attribute {@link android.R.attr#persistable} set true. + * + * @param savedInstanceState if the activity is being re-initialized after + * previously being shut down then this Bundle contains the data it most + * recently supplied in {@link #onSaveInstanceState}. + * <b><i>Note: Otherwise it is null.</i></b> + * @param persistentState if the activity is being re-initialized after + * previously being shut down or powered off then this Bundle contains the data it most + * recently supplied to outPersistentState in {@link #onSaveInstanceState}. + * <b><i>Note: Otherwise it is null.</i></b> + * + * @see #onCreate(android.os.Bundle) + * @see #onStart + * @see #onSaveInstanceState + * @see #onRestoreInstanceState + * @see #onPostCreate + */ + protected void onCreate(@Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { + onCreate(savedInstanceState); + } + + /** * The hook for {@link ActivityThread} to restore the state of this activity. * * Calls {@link #onSaveInstanceState(android.os.Bundle)} and @@ -935,6 +965,23 @@ public class Activity extends ContextThemeWrapper } /** + * The hook for {@link ActivityThread} to restore the state of this activity. + * + * Calls {@link #onSaveInstanceState(android.os.Bundle)} and + * {@link #restoreManagedDialogs(android.os.Bundle)}. + * + * @param savedInstanceState contains the saved state + * @param persistentState contains the persistable saved state + */ + final void performRestoreInstanceState(Bundle savedInstanceState, + PersistableBundle persistentState) { + onRestoreInstanceState(savedInstanceState, persistentState); + if (savedInstanceState != null) { + restoreManagedDialogs(savedInstanceState); + } + } + + /** * This method is called after {@link #onStart} when the activity is * being re-initialized from a previously saved state, given here in * <var>savedInstanceState</var>. Most implementations will simply use {@link #onCreate} @@ -962,7 +1009,34 @@ public class Activity extends ContextThemeWrapper } } } - + + /** + * This is the same as {@link #onRestoreInstanceState(Bundle)} but is called for activities + * created with the attribute {@link android.R.attr#persistable}. The {@link + * android.os.PersistableBundle} passed came from the restored PersistableBundle first + * saved in {@link #onSaveInstanceState(Bundle, PersistableBundle)}. + * + * <p>This method is called between {@link #onStart} and + * {@link #onPostCreate}. + * + * <p>If this method is called {@link #onRestoreInstanceState(Bundle)} will not be called. + * + * @param savedInstanceState the data most recently supplied in {@link #onSaveInstanceState}. + * @param persistentState the data most recently supplied in {@link #onSaveInstanceState}. + * + * @see #onRestoreInstanceState(Bundle) + * @see #onCreate + * @see #onPostCreate + * @see #onResume + * @see #onSaveInstanceState + */ + protected void onRestoreInstanceState(Bundle savedInstanceState, + PersistableBundle persistentState) { + if (savedInstanceState != null) { + onRestoreInstanceState(savedInstanceState); + } + } + /** * Restore the state of any saved managed dialogs. * @@ -1032,13 +1106,25 @@ public class Activity extends ContextThemeWrapper mTitleReady = true; onTitleChanged(getTitle(), getTitleColor()); } - if (mEnterTransitionCoordinator != null) { - mEnterTransitionCoordinator.readyToEnter(); - } mCalled = true; } /** + * This is the same as {@link #onPostCreate(Bundle)} but is called for activities + * created with the attribute {@link android.R.attr#persistable}. + * + * @param savedInstanceState The data most recently supplied in {@link #onSaveInstanceState} + * @param persistentState The data caming from the PersistableBundle first + * saved in {@link #onSaveInstanceState(Bundle, PersistableBundle)}. + * + * @see #onCreate + */ + protected void onPostCreate(@Nullable Bundle savedInstanceState, + @Nullable PersistableBundle persistentState) { + onPostCreate(savedInstanceState); + } + + /** * 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}. @@ -1115,7 +1201,7 @@ public class Activity extends ContextThemeWrapper protected void onResume() { if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this); getApplication().dispatchActivityResumed(this); - mCalledActivityOptions = null; + mActivityTransitionState.onResume(); mCalled = true; } @@ -1190,10 +1276,27 @@ public class Activity extends ContextThemeWrapper final void performSaveInstanceState(Bundle outState) { onSaveInstanceState(outState); saveManagedDialogs(outState); + mActivityTransitionState.saveState(outState); if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState); } /** + * The hook for {@link ActivityThread} to save the state of this activity. + * + * Calls {@link #onSaveInstanceState(android.os.Bundle)} + * and {@link #saveManagedDialogs(android.os.Bundle)}. + * + * @param outState The bundle to save the state to. + * @param outPersistentState The bundle to save persistent state to. + */ + final void performSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + onSaveInstanceState(outState, outPersistentState); + saveManagedDialogs(outState); + if (DEBUG_LIFECYCLE) Slog.v(TAG, "onSaveInstanceState " + this + ": " + outState + + ", " + outPersistentState); + } + + /** * Called to retrieve per-instance state from an activity before being killed * so that the state can be restored in {@link #onCreate} or * {@link #onRestoreInstanceState} (the {@link Bundle} populated by this method @@ -1248,6 +1351,25 @@ public class Activity extends ContextThemeWrapper } /** + * This is the same as {@link #onSaveInstanceState} but is called for activities + * created with the attribute {@link android.R.attr#persistable}. The {@link + * android.os.PersistableBundle} passed in will be saved and presented in + * {@link #onCreate(Bundle, PersistableBundle)} the first time that this activity + * is restarted following the next device reboot. + * + * @param outState Bundle in which to place your saved state. + * @param outPersistentState State which will be saved across reboots. + * + * @see #onSaveInstanceState(Bundle) + * @see #onCreate + * @see #onRestoreInstanceState(Bundle, PersistableBundle) + * @see #onPause + */ + protected void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) { + onSaveInstanceState(outState); + } + + /** * Save the state of any managed dialogs. * * @param outState place to store the saved state. @@ -1425,10 +1547,7 @@ 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; - } + mActivityTransitionState.onStop(); getApplication().dispatchActivityStopped(this); mTranslucentCallback = null; mCalled = true; @@ -1715,7 +1834,8 @@ public class Activity extends ContextThemeWrapper } } } - if (activity == null && children == null && fragments == null && !retainLoaders) { + if (activity == null && children == null && fragments == null && !retainLoaders + && mVoiceInteractor == null) { return null; } @@ -1724,6 +1844,7 @@ public class Activity extends ContextThemeWrapper nci.children = children; nci.fragments = fragments; nci.loaders = mAllLoaderManagers; + nci.voiceInteractor = mVoiceInteractor; return nci; } @@ -1954,15 +2075,16 @@ public class Activity extends ContextThemeWrapper * <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 + * @param toolbar Toolbar to set as the Activity's action bar */ - public void setActionBar(@Nullable Toolbar actionBar) { + public void setActionBar(@Nullable Toolbar toolbar) { 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); + mActionBar = new ToolbarActionBar(toolbar, getTitle(), this); + mActionBar.invalidateOptionsMenu(); } /** @@ -2328,8 +2450,12 @@ public class Activity extends ContextThemeWrapper * but you can override this to do whatever you want. */ public void onBackPressed() { + if (mActionBar != null && mActionBar.collapseActionView()) { + return; + } + if (!mFragments.popBackStackImmediate()) { - finishWithTransition(); + finishAfterTransition(); } } @@ -2539,6 +2665,14 @@ public class Activity extends ContextThemeWrapper */ public boolean dispatchKeyEvent(KeyEvent event) { onUserInteraction(); + + // Let action bars open menus in response to the menu key prioritized over + // the window handling it + if (event.getKeyCode() == KeyEvent.KEYCODE_MENU && + mActionBar != null && mActionBar.onMenuKeyEvent(event)) { + return true; + } + Window win = getWindow(); if (win.superDispatchKeyEvent(event)) { return true; @@ -2786,7 +2920,9 @@ public class Activity extends ContextThemeWrapper * time it needs to be displayed. */ public void invalidateOptionsMenu() { - mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); + if (mActionBar == null || !mActionBar.invalidateOptionsMenu()) { + mWindow.invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL); + } } /** @@ -2996,7 +3132,9 @@ public class Activity extends ContextThemeWrapper * open, this method does nothing. */ public void openOptionsMenu() { - mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null); + if (mActionBar == null || !mActionBar.openOptionsMenu()) { + mWindow.openPanel(Window.FEATURE_OPTIONS_PANEL, null); + } } /** @@ -3498,14 +3636,16 @@ public class Activity extends ContextThemeWrapper theme.applyStyle(resid, false); } - // Get the primary color and update the RecentsActivityValues for this activity - TypedArray a = getTheme().obtainStyledAttributes(com.android.internal.R.styleable.Theme); - int colorPrimary = a.getColor(com.android.internal.R.styleable.Theme_colorPrimary, 0); - a.recycle(); - if (colorPrimary != 0) { - ActivityManager.RecentsActivityValues v = new ActivityManager.RecentsActivityValues(); - v.colorPrimary = colorPrimary; - setRecentsActivityValues(v); + // Get the primary color and update the TaskDescription for this activity + if (theme != null) { + TypedArray a = theme.obtainStyledAttributes(com.android.internal.R.styleable.Theme); + int colorPrimary = a.getColor(com.android.internal.R.styleable.Theme_colorPrimary, 0); + a.recycle(); + if (colorPrimary != 0) { + ActivityManager.TaskDescription v = new ActivityManager.TaskDescription(null, null, + colorPrimary); + setTaskDescription(v); + } } } @@ -3524,7 +3664,7 @@ public class Activity extends ContextThemeWrapper public void startActivityForResult(Intent intent, int requestCode) { Bundle options = null; if (mWindow.hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { - options = ActivityOptions.makeSceneTransitionAnimation(mWindow, null).toBundle(); + options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle(); } startActivityForResult(intent, requestCode, options); } @@ -3565,9 +3705,7 @@ public class Activity extends ContextThemeWrapper */ public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) { if (options != null) { - ActivityOptions activityOptions = new ActivityOptions(options); - activityOptions.dispatchStartExit(); - mCalledActivityOptions = activityOptions; + mActivityTransitionState.startExitOutTransition(this, options); } if (mParent == null) { Instrumentation.ActivityResult ar = @@ -4079,7 +4217,11 @@ public class Activity extends ContextThemeWrapper */ public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, int requestCode) { - startActivityFromFragment(fragment, intent, requestCode, null); + Bundle options = null; + if (mWindow.hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + options = ActivityOptions.makeSceneTransitionAnimation(this).toBundle(); + } + startActivityFromFragment(fragment, intent, requestCode, options); } /** @@ -4104,6 +4246,9 @@ public class Activity extends ContextThemeWrapper */ public void startActivityFromFragment(@NonNull Fragment fragment, Intent intent, int requestCode, @Nullable Bundle options) { + if (options != null) { + mActivityTransitionState.startExitOutTransition(this, options); + } Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity( this, mMainThread.getApplicationThread(), mToken, fragment, @@ -4433,13 +4578,10 @@ public class Activity extends ContextThemeWrapper * 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) + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, android.util.Pair[]) */ - public void finishWithTransition() { - if (mEnterTransitionCoordinator != null) { - mEnterTransitionCoordinator.startExit(); - } else { + public void finishAfterTransition() { + if (!mActivityTransitionState.startExitBackTransition(this)) { finish(); } } @@ -4517,6 +4659,27 @@ public class Activity extends ContextThemeWrapper } /** + * Called when an activity you launched with an activity transition exposes this + * Activity through a returning activity transition, giving you the resultCode + * and any additional data from it. This method will only be called if the activity + * set a result code other than {@link #RESULT_CANCELED} and it supports activity + * transitions with {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * <p>The purpose of this function is to let the called Activity send a hint about + * its state so that this underlying Activity can prepare to be exposed. A call to + * this method does not guarantee that the called Activity has or will be exiting soon. + * It only indicates that it will expose this Activity's Window and it has + * some data to pass to prepare it.</p> + * + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + protected void onActivityReenter(int resultCode, Intent data) { + } + + /** * Create a new PendingIntent object which you can hand to others * for them to use to send result data back to your * {@link #onActivityResult} callback. The created object will be either @@ -4793,27 +4956,30 @@ public class Activity extends ContextThemeWrapper } /** - * Sets information describing this Activity for presentation inside the Recents System UI. When - * {@link ActivityManager#getRecentTasks} is called, the activities of each task are - * traversed in order from the topmost activity to the bottommost. The traversal continues for - * each property until a suitable value is found. For each task those values will be returned in - * {@link android.app.ActivityManager.RecentsActivityValues}. + * Sets information describing the task with this activity for presentation inside the Recents + * System UI. When {@link ActivityManager#getRecentTasks} is called, the activities of each task + * are traversed in order from the topmost activity to the bottommost. The traversal continues + * for each property until a suitable value is found. For each task the taskDescription will be + * returned in {@link android.app.ActivityManager.TaskDescription}. * * @see ActivityManager#getRecentTasks - * @see android.app.ActivityManager.RecentsActivityValues + * @see android.app.ActivityManager.TaskDescription * - * @param values The Recents values that describe this activity. + * @param taskDescription The TaskDescription properties that describe the task with this activity */ - public void setRecentsActivityValues(ActivityManager.RecentsActivityValues values) { - ActivityManager.RecentsActivityValues activityValues = - new ActivityManager.RecentsActivityValues(values); - // Scale the icon down to something reasonable - if (values.icon != null) { + public void setTaskDescription(ActivityManager.TaskDescription taskDescription) { + ActivityManager.TaskDescription td; + // Scale the icon down to something reasonable if it is provided + if (taskDescription.getIcon() != null) { final int size = ActivityManager.getLauncherLargeIconSizeInner(this); - activityValues.icon = Bitmap.createScaledBitmap(values.icon, size, size, true); + final Bitmap icon = Bitmap.createScaledBitmap(taskDescription.getIcon(), size, size, true); + td = new ActivityManager.TaskDescription(taskDescription.getLabel(), icon, + taskDescription.getPrimaryColor()); + } else { + td = taskDescription; } try { - ActivityManagerNative.getDefault().setRecentsActivityValues(mToken, activityValues); + ActivityManagerNative.getDefault().setTaskDescription(mToken, td); } catch (RemoteException e) { } } @@ -5120,7 +5286,8 @@ public class Activity extends ContextThemeWrapper * This call has no effect on non-translucent activities or on activities with the * {@link android.R.attr#windowIsFloating} attribute. * - * @see #convertToTranslucent(TranslucentConversionListener) + * @see #convertToTranslucent(android.app.Activity.TranslucentConversionListener, + * ActivityOptions) * @see TranslucentConversionListener * * @hide @@ -5151,19 +5318,29 @@ public class Activity extends ContextThemeWrapper * * @param callback the method to call when all visible Activities behind this one have been * drawn and it is safe to make this Activity translucent again. + * @param options activity options delivered to the activity below this one. The options + * are retrieved using {@link #getActivityOptions}. * * @see #convertFromTranslucent() * @see TranslucentConversionListener * * @hide */ - public void convertToTranslucent(TranslucentConversionListener callback) { + void convertToTranslucent(TranslucentConversionListener callback, ActivityOptions options) { + boolean drawComplete; try { mTranslucentCallback = callback; mChangeCanvasToTranslucent = - ActivityManagerNative.getDefault().convertToTranslucent(mToken); + ActivityManagerNative.getDefault().convertToTranslucent(mToken, options); + drawComplete = true; } catch (RemoteException e) { - // pass + // Make callback return as though it timed out. + mChangeCanvasToTranslucent = false; + drawComplete = false; + } + if (!mChangeCanvasToTranslucent && mTranslucentCallback != null) { + // Window is already translucent. + mTranslucentCallback.onTranslucentConversionComplete(drawComplete); } } @@ -5179,6 +5356,22 @@ public class Activity extends ContextThemeWrapper } /** + * Retrieve the ActivityOptions passed in from the launching activity or passed back + * from an activity launched by this activity in its call to {@link + * #convertToTranslucent(TranslucentConversionListener, ActivityOptions)} + * + * @return The ActivityOptions passed to {@link #convertToTranslucent}. + * @hide + */ + ActivityOptions getActivityOptions() { + try { + return ActivityManagerNative.getDefault().getActivityOptions(mToken); + } catch (RemoteException e) { + } + return null; + } + + /** * Adjust the current immersive mode setting. * * Note that changing this value will have no effect on the activity's @@ -5392,18 +5585,34 @@ 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}. + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, <var>listener</var> + * will be called to handle shared elements on the <i>launched</i> Activity. This requires + * {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param listener Used to manipulate shared element transitions on the launched Activity. + */ + public void setEnterSharedElementListener(SharedElementListener listener) { + if (listener == null) { + listener = SharedElementListener.NULL_LISTENER; + } + mEnterTransitionListener = listener; + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, <var>listener</var> + * will be called to handle shared elements on the <i>launching</i> Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. * - * @param listener Used to listen to events in the entering transition. + * @param listener Used to manipulate shared element transitions on the launching Activity. */ - public void setActivityTransitionListener(ActivityOptions.ActivityTransitionListener listener) { - if (mEnterTransitionCoordinator != null) { - mEnterTransitionCoordinator.setActivityTransitionListener(listener); + public void setExitSharedElementListener(SharedElementListener listener) { + if (listener == null) { + listener = SharedElementListener.NULL_LISTENER; } + mExitTransitionListener = listener; } // ------------------ Internal API ------------------ @@ -5412,30 +5621,12 @@ public class Activity extends ContextThemeWrapper mParent = parent; } - final void attach(Context context, ActivityThread aThread, Instrumentation instr, IBinder token, - Application application, Intent intent, ActivityInfo info, CharSequence title, - Activity parent, String id, NonConfigurationInstances lastNonConfigurationInstances, - Configuration config) { - attach(context, aThread, instr, token, 0, application, intent, info, title, parent, id, - lastNonConfigurationInstances, config); - } - - 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) { - 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) { + Configuration config, IVoiceInteractor voiceInteractor) { attachBaseContext(context); mFragments.attachActivity(this, mContainer, null); @@ -5464,8 +5655,14 @@ public class Activity extends ContextThemeWrapper mParent = parent; mEmbeddedID = id; mLastNonConfigurationInstances = lastNonConfigurationInstances; - mVoiceInteractor = voiceInteractor != null - ? new VoiceInteractor(this, this, voiceInteractor, Looper.myLooper()) : null; + if (voiceInteractor != null) { + if (lastNonConfigurationInstances != null) { + mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor; + } else { + mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this, + Looper.myLooper()); + } + } mWindow.setWindowManager( (WindowManager)context.getSystemService(Context.WINDOW_SERVICE), @@ -5476,12 +5673,6 @@ 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 */ @@ -5489,14 +5680,27 @@ public class Activity extends ContextThemeWrapper return mParent != null ? mParent.getActivityToken() : mToken; } - final void performCreate(Bundle icicle) { - onCreate(icicle); + final void performCreateCommon() { mVisibleFromClient = !mWindow.getWindowStyle().getBoolean( com.android.internal.R.styleable.Window_windowNoDisplay, false); mFragments.dispatchActivityCreated(); + mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions()); + } + + final void performCreate(Bundle icicle) { + onCreate(icicle); + mActivityTransitionState.readState(icicle); + performCreateCommon(); + } + + final void performCreate(Bundle icicle, PersistableBundle persistentState) { + onCreate(icicle, persistentState); + mActivityTransitionState.readState(icicle); + performCreateCommon(); } final void performStart() { + mActivityTransitionState.setEnterActivityOptions(this, getActivityOptions()); mFragments.noteStateNotSaved(); mCalled = false; mFragments.execPendingActions(); @@ -5519,6 +5723,7 @@ public class Activity extends ContextThemeWrapper lm.doReportStart(); } } + mActivityTransitionState.enterReady(this); } final void performRestart() { @@ -5666,6 +5871,9 @@ public class Activity extends ContextThemeWrapper if (mLoaderManager != null) { mLoaderManager.doDestroy(); } + if (mVoiceInteractor != null) { + mVoiceInteractor.detachActivity(); + } } /** @@ -5729,7 +5937,8 @@ public class Activity extends ContextThemeWrapper * have completed drawing. This is necessary only after an {@link Activity} has been made * opaque using {@link Activity#convertFromTranslucent()} and before it has been drawn * translucent again following a call to {@link - * Activity#convertToTranslucent(TranslucentConversionListener)}. + * Activity#convertToTranslucent(android.app.Activity.TranslucentConversionListener, + * ActivityOptions)} * * @hide */ @@ -5743,7 +5952,7 @@ public class Activity extends ContextThemeWrapper * occurred waiting for the Activity to complete drawing. * * @see Activity#convertFromTranslucent() - * @see Activity#convertToTranslucent(TranslucentConversionListener) + * @see Activity#convertToTranslucent(TranslucentConversionListener, ActivityOptions) */ public void onTranslucentConversionComplete(boolean drawComplete); } diff --git a/core/java/android/app/ActivityManager.aidl b/core/java/android/app/ActivityManager.aidl new file mode 100644 index 0000000..92350da --- /dev/null +++ b/core/java/android/app/ActivityManager.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.app; + +parcelable ActivityManager.RecentTaskInfo; diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 044727d..788ac56 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -33,6 +33,7 @@ import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.Color; import android.graphics.Rect; import android.os.Bundle; import android.os.Debug; @@ -52,6 +53,7 @@ import android.util.Slog; import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -474,67 +476,86 @@ public class ActivityManager { } /** - * Information you can set and retrieve about the current activity within Recents. + * Information you can set and retrieve about the current activity within the recent task list. */ - public static class RecentsActivityValues implements Parcelable { - public CharSequence label; - public Bitmap icon; - public int colorPrimary; - - public RecentsActivityValues(RecentsActivityValues values) { - copyFrom(values); - } + public static class TaskDescription implements Parcelable { + private String mLabel; + private Bitmap mIcon; + private int mColorPrimary; /** - * Creates the RecentsActivityValues to the specified values. + * Creates the TaskDescription to the specified values. * - * @param label A label and description of the current state of this activity. - * @param icon An icon that represents the current state of this activity. - * @param color A color to override the theme's primary color. + * @param label A label and description of the current state of this task. + * @param icon An icon that represents the current state of this task. + * @param colorPrimary A color to override the theme's primary color. This color must be opaque. */ - public RecentsActivityValues(CharSequence label, Bitmap icon, int color) { - this.label = label; - this.icon = icon; - this.colorPrimary = color; + public TaskDescription(String label, Bitmap icon, int colorPrimary) { + if ((colorPrimary != 0) && (Color.alpha(colorPrimary) != 255)) { + throw new RuntimeException("A TaskDescription's primary color should be opaque"); + } + + mLabel = label; + mIcon = icon; + mColorPrimary = colorPrimary; } /** - * Creates the RecentsActivityValues to the specified values. + * Creates the TaskDescription to the specified values. * * @param label A label and description of the current state of this activity. * @param icon An icon that represents the current state of this activity. */ - public RecentsActivityValues(CharSequence label, Bitmap icon) { + public TaskDescription(String label, Bitmap icon) { this(label, icon, 0); } /** - * Creates the RecentsActivityValues to the specified values. + * Creates the TaskDescription to the specified values. * * @param label A label and description of the current state of this activity. */ - public RecentsActivityValues(CharSequence label) { + public TaskDescription(String label) { this(label, null, 0); } - public RecentsActivityValues() { + /** + * Creates an empty TaskDescription. + */ + public TaskDescription() { this(null, null, 0); } - private RecentsActivityValues(Parcel source) { + /** + * Creates a copy of another TaskDescription. + */ + public TaskDescription(TaskDescription td) { + this(td.getLabel(), td.getIcon(), td.getPrimaryColor()); + } + + private TaskDescription(Parcel source) { readFromParcel(source); } /** - * Do a shallow copy of another set of activity values. - * @hide + * @return The label and description of the current state of this task. */ - public void copyFrom(RecentsActivityValues v) { - if (v != null) { - label = v.label; - icon = v.icon; - colorPrimary = v.colorPrimary; - } + public String getLabel() { + return mLabel; + } + + /** + * @return The icon that represents the current state of this task. + */ + public Bitmap getIcon() { + return mIcon; + } + + /** + * @return The color override on the theme's primary color. + */ + public int getPrimaryColor() { + return mColorPrimary; } @Override @@ -544,37 +565,41 @@ public class ActivityManager { @Override public void writeToParcel(Parcel dest, int flags) { - TextUtils.writeToParcel(label, dest, - Parcelable.PARCELABLE_WRITE_RETURN_VALUE); - if (icon == null) { + if (mLabel == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + dest.writeString(mLabel); + } + if (mIcon == null) { dest.writeInt(0); } else { dest.writeInt(1); - icon.writeToParcel(dest, 0); + mIcon.writeToParcel(dest, 0); } - dest.writeInt(colorPrimary); + dest.writeInt(mColorPrimary); } public void readFromParcel(Parcel source) { - label = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - icon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null; - colorPrimary = source.readInt(); + mLabel = source.readInt() > 0 ? source.readString() : null; + mIcon = source.readInt() > 0 ? Bitmap.CREATOR.createFromParcel(source) : null; + mColorPrimary = source.readInt(); } - public static final Creator<RecentsActivityValues> CREATOR - = new Creator<RecentsActivityValues>() { - public RecentsActivityValues createFromParcel(Parcel source) { - return new RecentsActivityValues(source); + public static final Creator<TaskDescription> CREATOR + = new Creator<TaskDescription>() { + public TaskDescription createFromParcel(Parcel source) { + return new TaskDescription(source); } - public RecentsActivityValues[] newArray(int size) { - return new RecentsActivityValues[size]; + public TaskDescription[] newArray(int size) { + return new TaskDescription[size]; } }; @Override public String toString() { - return "RecentsActivityValues Label: " + label + " Icon: " + icon + - " colorPrimary: " + colorPrimary; + return "TaskDescription Label: " + mLabel + " Icon: " + mIcon + + " colorPrimary: " + mColorPrimary; } } @@ -628,9 +653,11 @@ public class ActivityManager { /** * The recent activity values for the highest activity in the stack to have set the values. - * {@link Activity#setRecentsActivityValues(android.app.ActivityManager.RecentsActivityValues)}. + * {@link Activity#setTaskDescription(android.app.ActivityManager.TaskDescription)}. + * + * @hide */ - public RecentsActivityValues activityValues; + public TaskDescription taskDescription; public RecentTaskInfo() { } @@ -653,9 +680,9 @@ public class ActivityManager { ComponentName.writeToParcel(origActivity, dest); TextUtils.writeToParcel(description, dest, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); - if (activityValues != null) { + if (taskDescription != null) { dest.writeInt(1); - activityValues.writeToParcel(dest, 0); + taskDescription.writeToParcel(dest, 0); } else { dest.writeInt(0); } @@ -669,8 +696,8 @@ public class ActivityManager { baseIntent = source.readInt() > 0 ? Intent.CREATOR.createFromParcel(source) : null; origActivity = ComponentName.readFromParcel(source); description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source); - activityValues = source.readInt() > 0 ? - RecentsActivityValues.CREATOR.createFromParcel(source) : null; + taskDescription = source.readInt() > 0 ? + TaskDescription.CREATOR.createFromParcel(source) : null; stackId = source.readInt(); userId = source.readInt(); } @@ -711,7 +738,7 @@ public class ActivityManager { public static final int RECENT_INCLUDE_PROFILES = 0x0004; /** - * Return a list of the tasks that the user has recently launched, with + * <p></p>Return a list of the tasks that the user has recently launched, with * the most recent being first and older ones after in order. * * <p><b>Note: this method is only intended for debugging and presenting @@ -723,6 +750,15 @@ public class ActivityManager { * same time, assumptions made about the meaning of the data here for * purposes of control flow will be incorrect.</p> * + * @deprecated As of {@link android.os.Build.VERSION_CODES#L}, this method is + * no longer available to third party applications: as the introduction of + * document-centric recents means + * it can leak personal information to the caller. For backwards compatibility, + * it will still return a small subset of its data: at least the caller's + * own tasks (though see {@link #getAppTasks()} for the correct supported + * way to retrieve that information), and possibly some other tasks + * such as home that are known to not be sensitive. + * * @param maxNum The maximum number of entries to return in the list. The * actual number returned may be smaller, depending on how many tasks the * user has started and the maximum number the system can remember. @@ -735,6 +771,7 @@ public class ActivityManager { * @throws SecurityException Throws SecurityException if the caller does * not hold the {@link android.Manifest.permission#GET_TASKS} permission. */ + @Deprecated public List<RecentTaskInfo> getRecentTasks(int maxNum, int flags) throws SecurityException { try { @@ -879,7 +916,29 @@ public class ActivityManager { readFromParcel(source); } } - + + /** + * Get the list of tasks associated with the calling application. + * + * @return The list of tasks associated with the application making this call. + * @throws SecurityException + */ + public List<ActivityManager.AppTask> getAppTasks() { + ArrayList<AppTask> tasks = new ArrayList<AppTask>(); + List<IAppTask> appTasks; + try { + appTasks = ActivityManagerNative.getDefault().getAppTasks(); + } catch (RemoteException e) { + // System dead, we will be dead too soon! + return null; + } + int numAppTasks = appTasks.size(); + for (int i = 0; i < numAppTasks; i++) { + tasks.add(new AppTask(appTasks.get(i))); + } + return tasks; + } + /** * Return a list of the tasks that are currently running, with * the most recent being first and older ones after in order. Note that @@ -897,6 +956,14 @@ public class ActivityManager { * same time, assumptions made about the meaning of the data here for * purposes of control flow will be incorrect.</p> * + * @deprecated As of {@link android.os.Build.VERSION_CODES#L}, this method + * is no longer available to third party + * applications: the introduction of document-centric recents means + * it can leak person information to the caller. For backwards compatibility, + * it will still retu rn a small subset of its data: at least the caller's + * own tasks, and possibly some other tasks + * such as home that are known to not be sensitive. + * * @param maxNum The maximum number of entries to return in the list. The * actual number returned may be smaller, depending on how many tasks the * user has started. @@ -907,6 +974,7 @@ public class ActivityManager { * @throws SecurityException Throws SecurityException if the caller does * not hold the {@link android.Manifest.permission#GET_TASKS} permission. */ + @Deprecated public List<RunningTaskInfo> getRunningTasks(int maxNum) throws SecurityException { try { @@ -2382,4 +2450,42 @@ public class ActivityManager { return false; } } + + /** + * The AppTask allows you to manage your own application's tasks. + * See {@link android.app.ActivityManager#getAppTasks()} + */ + public static class AppTask { + private IAppTask mAppTaskImpl; + + /** @hide */ + public AppTask(IAppTask task) { + mAppTaskImpl = task; + } + + /** + * Finishes all activities in this task and removes it from the recent tasks list. + */ + public void finishAndRemoveTask() { + try { + mAppTaskImpl.finishAndRemoveTask(); + } catch (RemoteException e) { + Slog.e(TAG, "Invalid AppTask", e); + } + } + + /** + * Get the RecentTaskInfo associated with this task. + * + * @return The RecentTaskInfo for this task, or null if the task no longer exists. + */ + public RecentTaskInfo getTaskInfo() { + try { + return mAppTaskImpl.getTaskInfo(); + } catch (RemoteException e) { + Slog.e(TAG, "Invalid AppTask", e); + return null; + } + } + } } diff --git a/core/java/android/app/ActivityManagerNative.java b/core/java/android/app/ActivityManagerNative.java index 57da21e..56462ae 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -40,6 +40,7 @@ import android.os.IBinder; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.ServiceManager; import android.os.StrictMode; @@ -454,7 +455,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM case ACTIVITY_PAUSED_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); - activityPaused(token); + PersistableBundle persistentState = data.readPersistableBundle(); + activityPaused(token, persistentState); reply.writeNoException(); return true; } @@ -463,10 +465,9 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); Bundle map = data.readBundle(); - Bitmap thumbnail = data.readInt() != 0 - ? Bitmap.CREATOR.createFromParcel(data) : null; + PersistableBundle persistentState = data.readPersistableBundle(); CharSequence description = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(data); - activityStopped(token, map, thumbnail, description); + activityStopped(token, map, persistentState, description); reply.writeNoException(); return true; } @@ -505,6 +506,20 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } + case GET_APP_TASKS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + List<IAppTask> list = getAppTasks(); + reply.writeNoException(); + int N = list != null ? list.size() : -1; + reply.writeInt(N); + int i; + for (i=0; i<N; i++) { + IAppTask task = list.get(i); + reply.writeStrongBinder(task.asBinder()); + } + return true; + } + case GET_TASKS_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); int maxNum = data.readInt(); @@ -928,7 +943,9 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM b = data.readStrongBinder(); IUiAutomationConnection c = IUiAutomationConnection.Stub.asInterface(b); int userId = data.readInt(); - boolean res = startInstrumentation(className, profileFile, fl, arguments, w, c, userId); + String abiOverride = data.readString(); + boolean res = startInstrumentation(className, profileFile, fl, arguments, w, c, userId, + abiOverride); reply.writeNoException(); reply.writeInt(res ? 1 : 0); return true; @@ -1113,7 +1130,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM int pid = data.readInt(); int uid = data.readInt(); int mode = data.readInt(); - int res = checkUriPermission(uri, pid, uid, mode); + int userId = data.readInt(); + int res = checkUriPermission(uri, pid, uid, mode, userId); reply.writeNoException(); reply.writeInt(res); return true; @@ -1138,7 +1156,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM String targetPkg = data.readString(); Uri uri = Uri.CREATOR.createFromParcel(data); int mode = data.readInt(); - grantUriPermission(app, targetPkg, uri, mode); + int userId = data.readInt(); + grantUriPermission(app, targetPkg, uri, mode, userId); reply.writeNoException(); return true; } @@ -1149,7 +1168,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM IApplicationThread app = ApplicationThreadNative.asInterface(b); Uri uri = Uri.CREATOR.createFromParcel(data); int mode = data.readInt(); - revokeUriPermission(app, uri, mode); + int userId = data.readInt(); + revokeUriPermission(app, uri, mode, userId); reply.writeNoException(); return true; } @@ -1158,7 +1178,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); Uri uri = Uri.CREATOR.createFromParcel(data); int mode = data.readInt(); - takePersistableUriPermission(uri, mode); + int userId = data.readInt(); + takePersistableUriPermission(uri, mode, userId); reply.writeNoException(); return true; } @@ -1167,7 +1188,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM data.enforceInterface(IActivityManager.descriptor); Uri uri = Uri.CREATOR.createFromParcel(data); int mode = data.readInt(); - releasePersistableUriPermission(uri, mode); + int userId = data.readInt(); + releasePersistableUriPermission(uri, mode, userId); reply.writeNoException(); return true; } @@ -1541,12 +1563,28 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM case CONVERT_TO_TRANSLUCENT_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); - boolean converted = convertToTranslucent(token); + final Bundle bundle; + if (data.readInt() == 0) { + bundle = null; + } else { + bundle = data.readBundle(); + } + final ActivityOptions options = bundle == null ? null : new ActivityOptions(bundle); + boolean converted = convertToTranslucent(token, options); reply.writeNoException(); reply.writeInt(converted ? 1 : 0); return true; } + case GET_ACTIVITY_OPTIONS_TRANSACTION: { + data.enforceInterface(IActivityManager.descriptor); + IBinder token = data.readStrongBinder(); + final ActivityOptions options = getActivityOptions(token); + reply.writeNoException(); + reply.writeBundle(options == null ? null : options.toBundle()); + return true; + } + case SET_IMMERSIVE_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); @@ -1601,7 +1639,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM String targetPkg = data.readString(); Uri uri = Uri.CREATOR.createFromParcel(data); int mode = data.readInt(); - grantUriPermissionFromOwner(owner, fromUid, targetPkg, uri, mode); + int userId = data.readInt(); + grantUriPermissionFromOwner(owner, fromUid, targetPkg, uri, mode, userId); reply.writeNoException(); return true; } @@ -1611,10 +1650,11 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM IBinder owner = data.readStrongBinder(); Uri uri = null; if (data.readInt() != 0) { - Uri.CREATOR.createFromParcel(data); + uri = Uri.CREATOR.createFromParcel(data); } int mode = data.readInt(); - revokeUriPermissionFromOwner(owner, uri, mode); + int userId = data.readInt(); + revokeUriPermissionFromOwner(owner, uri, mode, userId); reply.writeNoException(); return true; } @@ -1625,7 +1665,8 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM String targetPkg = data.readString(); Uri uri = Uri.CREATOR.createFromParcel(data); int modeFlags = data.readInt(); - int res = checkGrantUriPermission(callingUid, targetPkg, uri, modeFlags); + int userId = data.readInt(); + int res = checkGrantUriPermission(callingUid, targetPkg, uri, modeFlags, userId); reply.writeNoException(); reply.writeInt(res); return true; @@ -2119,12 +2160,12 @@ public abstract class ActivityManagerNative extends Binder implements IActivityM return true; } - case SET_RECENTS_ACTIVITY_VALUES_TRANSACTION: { + case SET_TASK_DESCRIPTION_TRANSACTION: { data.enforceInterface(IActivityManager.descriptor); IBinder token = data.readStrongBinder(); - ActivityManager.RecentsActivityValues values = - ActivityManager.RecentsActivityValues.CREATOR.createFromParcel(data); - setRecentsActivityValues(token, values); + ActivityManager.TaskDescription values = + ActivityManager.TaskDescription.CREATOR.createFromParcel(data); + setTaskDescription(token, values); reply.writeNoException(); return true; } @@ -2583,31 +2624,27 @@ class ActivityManagerProxy implements IActivityManager data.recycle(); reply.recycle(); } - public void activityPaused(IBinder token) throws RemoteException + public void activityPaused(IBinder token, PersistableBundle persistentState) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); + data.writePersistableBundle(persistentState); mRemote.transact(ACTIVITY_PAUSED_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); reply.recycle(); } public void activityStopped(IBinder token, Bundle state, - Bitmap thumbnail, CharSequence description) throws RemoteException + PersistableBundle persistentState, CharSequence description) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); data.writeBundle(state); - if (thumbnail != null) { - data.writeInt(1); - thumbnail.writeToParcel(data, 0); - } else { - data.writeInt(0); - } + data.writePersistableBundle(persistentState); TextUtils.writeToParcel(description, data, 0); mRemote.transact(ACTIVITY_STOPPED_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); @@ -2662,6 +2699,26 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return res; } + public List<IAppTask> getAppTasks() throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + mRemote.transact(GET_APP_TASKS_TRANSACTION, data, reply, 0); + reply.readException(); + ArrayList<IAppTask> list = null; + int N = reply.readInt(); + if (N >= 0) { + list = new ArrayList<IAppTask>(); + while (N > 0) { + IAppTask task = IAppTask.Stub.asInterface(reply.readStrongBinder()); + list.add(task); + N--; + } + } + data.recycle(); + reply.recycle(); + return list; + } public List getTasks(int maxNum, int flags) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -2677,7 +2734,7 @@ class ActivityManagerProxy implements IActivityManager while (N > 0) { ActivityManager.RunningTaskInfo info = ActivityManager.RunningTaskInfo.CREATOR - .createFromParcel(reply); + .createFromParcel(reply); list.add(info); N--; } @@ -3284,7 +3341,8 @@ class ActivityManagerProxy implements IActivityManager public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, IInstrumentationWatcher watcher, - IUiAutomationConnection connection, int userId) throws RemoteException { + IUiAutomationConnection connection, int userId, String instructionSet) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -3295,6 +3353,7 @@ class ActivityManagerProxy implements IActivityManager data.writeStrongBinder(watcher != null ? watcher.asBinder() : null); data.writeStrongBinder(connection != null ? connection.asBinder() : null); data.writeInt(userId); + data.writeString(instructionSet); mRemote.transact(START_INSTRUMENTATION_TRANSACTION, data, reply, 0); reply.readException(); boolean res = reply.readInt() != 0; @@ -3543,7 +3602,7 @@ class ActivityManagerProxy implements IActivityManager reply.recycle(); return res; } - public int checkUriPermission(Uri uri, int pid, int uid, int mode) + public int checkUriPermission(Uri uri, int pid, int uid, int mode, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); @@ -3552,6 +3611,7 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(pid); data.writeInt(uid); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(CHECK_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); int res = reply.readInt(); @@ -3560,7 +3620,7 @@ class ActivityManagerProxy implements IActivityManager return res; } public void grantUriPermission(IApplicationThread caller, String targetPkg, - Uri uri, int mode) throws RemoteException { + Uri uri, int mode, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -3568,19 +3628,21 @@ class ActivityManagerProxy implements IActivityManager data.writeString(targetPkg); uri.writeToParcel(data, 0); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(GRANT_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); reply.recycle(); } public void revokeUriPermission(IApplicationThread caller, Uri uri, - int mode) throws RemoteException { + int mode, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller.asBinder()); uri.writeToParcel(data, 0); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(REVOKE_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -3588,12 +3650,14 @@ class ActivityManagerProxy implements IActivityManager } @Override - public void takePersistableUriPermission(Uri uri, int mode) throws RemoteException { + public void takePersistableUriPermission(Uri uri, int mode, int userId) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); uri.writeToParcel(data, 0); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(TAKE_PERSISTABLE_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -3601,12 +3665,14 @@ class ActivityManagerProxy implements IActivityManager } @Override - public void releasePersistableUriPermission(Uri uri, int mode) throws RemoteException { + public void releasePersistableUriPermission(Uri uri, int mode, int userId) + throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); uri.writeToParcel(data, 0); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(RELEASE_PERSISTABLE_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -4062,12 +4128,18 @@ class ActivityManagerProxy implements IActivityManager return res; } - public boolean convertToTranslucent(IBinder token) + public boolean convertToTranslucent(IBinder token, ActivityOptions options) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); + if (options == null) { + data.writeInt(0); + } else { + data.writeInt(1); + data.writeBundle(options.toBundle()); + } mRemote.transact(CONVERT_TO_TRANSLUCENT_TRANSACTION, data, reply, 0); reply.readException(); boolean res = reply.readInt() != 0; @@ -4076,6 +4148,20 @@ class ActivityManagerProxy implements IActivityManager return res; } + public ActivityOptions getActivityOptions(IBinder token) throws RemoteException { + Parcel data = Parcel.obtain(); + Parcel reply = Parcel.obtain(); + data.writeInterfaceToken(IActivityManager.descriptor); + data.writeStrongBinder(token); + mRemote.transact(GET_ACTIVITY_OPTIONS_TRANSACTION, data, reply, 0); + reply.readException(); + Bundle bundle = reply.readBundle(); + ActivityOptions options = bundle == null ? null : new ActivityOptions(bundle); + data.recycle(); + reply.recycle(); + return options; + } + public void setImmersive(IBinder token, boolean immersive) throws RemoteException { Parcel data = Parcel.obtain(); @@ -4160,7 +4246,7 @@ class ActivityManagerProxy implements IActivityManager } public void grantUriPermissionFromOwner(IBinder owner, int fromUid, String targetPkg, - Uri uri, int mode) throws RemoteException { + Uri uri, int mode, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -4169,6 +4255,7 @@ class ActivityManagerProxy implements IActivityManager data.writeString(targetPkg); uri.writeToParcel(data, 0); data.writeInt(mode); + data.writeInt(userId); mRemote.transact(GRANT_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -4176,7 +4263,7 @@ class ActivityManagerProxy implements IActivityManager } public void revokeUriPermissionFromOwner(IBinder owner, Uri uri, - int mode) throws RemoteException { + int mode, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -4188,6 +4275,7 @@ class ActivityManagerProxy implements IActivityManager data.writeInt(0); } data.writeInt(mode); + data.writeInt(userId); mRemote.transact(REVOKE_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); data.recycle(); @@ -4195,7 +4283,7 @@ class ActivityManagerProxy implements IActivityManager } public int checkGrantUriPermission(int callingUid, String targetPkg, - Uri uri, int modeFlags) throws RemoteException { + Uri uri, int modeFlags, int userId) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); @@ -4203,6 +4291,7 @@ class ActivityManagerProxy implements IActivityManager data.writeString(targetPkg); uri.writeToParcel(data, 0); data.writeInt(modeFlags); + data.writeInt(userId); mRemote.transact(CHECK_GRANT_URI_PERMISSION_TRANSACTION, data, reply, 0); reply.readException(); int res = reply.readInt(); @@ -4882,14 +4971,14 @@ class ActivityManagerProxy implements IActivityManager } @Override - public void setRecentsActivityValues(IBinder token, ActivityManager.RecentsActivityValues values) + public void setTaskDescription(IBinder token, ActivityManager.TaskDescription values) throws RemoteException { Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(token); values.writeToParcel(data, 0); - mRemote.transact(SET_RECENTS_ACTIVITY_VALUES_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); + mRemote.transact(SET_TASK_DESCRIPTION_TRANSACTION, data, reply, IBinder.FLAG_ONEWAY); reply.readException(); data.recycle(); reply.recycle(); diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index a49359f..a057c3e 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -17,18 +17,18 @@ package android.app; import android.content.Context; +import android.content.Intent; import android.graphics.Bitmap; 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.ArrayList; import java.util.List; import java.util.Map; @@ -108,6 +108,12 @@ public class ActivityOptions { private static final String KEY_TRANSITION_COMPLETE_LISTENER = "android:transitionCompleteListener"; + private static final String KEY_TRANSITION_IS_RETURNING = "android:transitionIsReturning"; + private static final String KEY_TRANSITION_SHARED_ELEMENTS = "android:sharedElementNames"; + private static final String KEY_LOCAL_SHARED_ELEMENTS = "android:localSharedElementNames"; + private static final String KEY_RESULT_DATA = "android:resultData"; + private static final String KEY_RESULT_CODE = "android:resultCode"; + /** @hide */ public static final int ANIM_NONE = 0; /** @hide */ @@ -131,7 +137,12 @@ public class ActivityOptions { private int mStartWidth; private int mStartHeight; private IRemoteCallback mAnimationStartedListener; - private ResultReceiver mExitReceiver; + private ResultReceiver mTransitionReceiver; + private boolean mIsReturning; + private ArrayList<String> mSharedElementNames; + private ArrayList<String> mLocalSharedElementNames; + private Intent mResultData; + private int mResultCode; /** * Create an ActivityOptions specifying a custom animation to run when @@ -334,7 +345,7 @@ public class ActivityOptions { * <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 activity The Activity whose window contains the 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 @@ -344,36 +355,70 @@ public class ActivityOptions { * @see android.transition.Transition#setEpicenterCallback( * android.transition.Transition.EpicenterCallback) */ - public static ActivityOptions makeSceneTransitionAnimation(Window window, + public static ActivityOptions makeSceneTransitionAnimation(Activity activity, View sharedElement, String sharedElementName) { - return makeSceneTransitionAnimation(window, - new SharedElementMappingListener(sharedElement, sharedElementName)); + return makeSceneTransitionAnimation(activity, Pair.create(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()} + * Activity. The position of the first element in sharedElements * 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. + * @param activity The Activity whose window contains the shared elements. + * @param sharedElements The names of the shared elements to transfer to the called + * Activity and their associated Views. The Views must each have + * a unique shared element name. * @return Returns a new ActivityOptions object that you can use to * supply these options as the options Bundle when starting an activity. + * Returns null if the Window does not have {@link Window#FEATURE_CONTENT_TRANSITIONS}. * @see android.transition.Transition#setEpicenterCallback( * android.transition.Transition.EpicenterCallback) */ - public static ActivityOptions makeSceneTransitionAnimation(Window window, - ActivityTransitionListener listener) { + public static ActivityOptions makeSceneTransitionAnimation(Activity activity, + Pair<View, String>... sharedElements) { + if (!activity.getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + return null; + } ActivityOptions opts = new ActivityOptions(); opts.mAnimationType = ANIM_SCENE_TRANSITION; - ExitTransitionCoordinator exit = new ExitTransitionCoordinator(window, listener); - opts.mExitReceiver = exit; + + ArrayList<String> names = new ArrayList<String>(); + ArrayList<String> mappedNames = new ArrayList<String>(); + + if (sharedElements != null) { + for (int i = 0; i < sharedElements.length; i++) { + Pair<View, String> sharedElement = sharedElements[i]; + names.add(sharedElement.second); + mappedNames.add(sharedElement.first.getViewName()); + } + } + + ExitTransitionCoordinator exit = new ExitTransitionCoordinator(activity, names, names, + mappedNames, false); + opts.mTransitionReceiver = exit; + opts.mSharedElementNames = names; + opts.mLocalSharedElementNames = mappedNames; + opts.mIsReturning = false; + return opts; + } + + /** @hide */ + public static ActivityOptions makeSceneTransitionAnimation(Activity activity, + ExitTransitionCoordinator exitCoordinator, ArrayList<String> sharedElementNames, + int resultCode, Intent resultData) { + ActivityOptions opts = new ActivityOptions(); + opts.mAnimationType = ANIM_SCENE_TRANSITION; + opts.mSharedElementNames = sharedElementNames; + opts.mTransitionReceiver = exitCoordinator; + opts.mIsReturning = true; + opts.mResultCode = resultCode; + opts.mResultData = resultData; return opts; } @@ -409,7 +454,12 @@ public class ActivityOptions { break; case ANIM_SCENE_TRANSITION: - mExitReceiver = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER); + mTransitionReceiver = opts.getParcelable(KEY_TRANSITION_COMPLETE_LISTENER); + mIsReturning = opts.getBoolean(KEY_TRANSITION_IS_RETURNING, false); + mSharedElementNames = opts.getStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS); + mLocalSharedElementNames = opts.getStringArrayList(KEY_LOCAL_SHARED_ELEMENTS); + mResultData = opts.getParcelable(KEY_RESULT_DATA); + mResultCode = opts.getInt(KEY_RESULT_CODE); break; } } @@ -466,15 +516,15 @@ public class ActivityOptions { /** @hide */ public void dispatchActivityStopped() { - if (mExitReceiver != null) { - mExitReceiver.send(ActivityTransitionCoordinator.MSG_ACTIVITY_STOPPED, null); + if (mTransitionReceiver != null) { + mTransitionReceiver.send(ActivityTransitionCoordinator.MSG_ACTIVITY_STOPPED, null); } } /** @hide */ public void dispatchStartExit() { - if (mExitReceiver != null) { - mExitReceiver.send(ActivityTransitionCoordinator.MSG_START_EXIT_TRANSITION, null); + if (mTransitionReceiver != null) { + mTransitionReceiver.send(ActivityTransitionCoordinator.MSG_START_EXIT_TRANSITION, null); } } @@ -489,19 +539,37 @@ public class ActivityOptions { } /** @hide */ - public static void abort(Bundle options) { - if (options != null) { - (new ActivityOptions(options)).abort(); - } + public void setReturning() { + mIsReturning = true; } /** @hide */ - public EnterTransitionCoordinator createEnterActivityTransition(Activity activity) { - EnterTransitionCoordinator coordinator = null; - if (mAnimationType == ANIM_SCENE_TRANSITION) { - coordinator = new EnterTransitionCoordinator(activity, mExitReceiver); + public boolean isReturning() { + return mIsReturning; + } + + /** @hide */ + public ArrayList<String> getSharedElementNames() { + return mSharedElementNames; + } + + /** @hide */ + public ArrayList<String> getLocalSharedElementNames() { return mLocalSharedElementNames; } + + /** @hide */ + public ResultReceiver getResultReceiver() { return mTransitionReceiver; } + + /** @hide */ + public int getResultCode() { return mResultCode; } + + /** @hide */ + public Intent getResultData() { return mResultData; } + + /** @hide */ + public static void abort(Bundle options) { + if (options != null) { + (new ActivityOptions(options)).abort(); } - return coordinator; } /** @@ -513,7 +581,12 @@ public class ActivityOptions { if (otherOptions.mPackageName != null) { mPackageName = otherOptions.mPackageName; } - mExitReceiver = null; + mTransitionReceiver = null; + mSharedElementNames = null; + mLocalSharedElementNames = null; + mIsReturning = false; + mResultData = null; + mResultCode = 0; switch (otherOptions.mAnimationType) { case ANIM_CUSTOM: mAnimationType = otherOptions.mAnimationType; @@ -558,9 +631,14 @@ public class ActivityOptions { break; case ANIM_SCENE_TRANSITION: mAnimationType = otherOptions.mAnimationType; - mExitReceiver = otherOptions.mExitReceiver; + mTransitionReceiver = otherOptions.mTransitionReceiver; + mSharedElementNames = otherOptions.mSharedElementNames; + mLocalSharedElementNames = otherOptions.mLocalSharedElementNames; + mIsReturning = otherOptions.mIsReturning; mThumbnail = null; mAnimationStartedListener = null; + mResultData = otherOptions.mResultData; + mResultCode = otherOptions.mResultCode; break; } } @@ -604,9 +682,14 @@ public class ActivityOptions { break; case ANIM_SCENE_TRANSITION: b.putInt(KEY_ANIM_TYPE, mAnimationType); - if (mExitReceiver != null) { - b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mExitReceiver); + if (mTransitionReceiver != null) { + b.putParcelable(KEY_TRANSITION_COMPLETE_LISTENER, mTransitionReceiver); } + b.putBoolean(KEY_TRANSITION_IS_RETURNING, mIsReturning); + b.putStringArrayList(KEY_TRANSITION_SHARED_ELEMENTS, mSharedElementNames); + b.putStringArrayList(KEY_LOCAL_SHARED_ELEMENTS, mLocalSharedElementNames); + b.putParcelable(KEY_RESULT_DATA, mResultData); + b.putInt(KEY_RESULT_CODE, mResultCode); break; } return b; @@ -626,126 +709,4 @@ public class ActivityOptions { 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 b606088..d9adba3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -47,6 +47,7 @@ import android.hardware.display.DisplayManagerGlobal; import android.net.IConnectivityManager; import android.net.Proxy; import android.net.ProxyInfo; +import android.net.Uri; import android.opengl.GLUtils; import android.os.AsyncTask; import android.os.Binder; @@ -56,11 +57,11 @@ 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; import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -69,8 +70,6 @@ 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; @@ -78,7 +77,6 @@ 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; @@ -268,6 +266,7 @@ public final class ActivityThread { Intent intent; IVoiceInteractor voiceInteractor; Bundle state; + PersistableBundle persistentState; Activity activity; Window window; Activity parent; @@ -295,7 +294,6 @@ public final class ActivityThread { boolean isForward; int pendingConfigChanges; boolean onlyLocalRequest; - Bundle activityOptions; View mPendingRemoveWindow; WindowManager mPendingRemoveWindowManager; @@ -317,6 +315,10 @@ public final class ActivityThread { return false; } + public boolean isPersistable() { + return (activityInfo.flags & ActivityInfo.FLAG_PERSISTABLE) != 0; + } + public String toString() { ComponentName componentName = intent != null ? intent.getComponent() : null; return "ActivityRecord{" @@ -590,8 +592,7 @@ public final class ActivityThread { public final void scheduleResumeActivity(IBinder token, int processState, boolean isForward, Bundle resumeArgs) { updateProcessState(processState, false); - sendMessage(H.RESUME_ACTIVITY, new Pair<IBinder, Bundle>(token, resumeArgs), - isForward ? 1 : 0); + sendMessage(H.RESUME_ACTIVITY, token, isForward ? 1 : 0); } public final void scheduleSendResult(IBinder token, List<ResultInfo> results) { @@ -605,11 +606,10 @@ 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, + IVoiceInteractor voiceInteractor, int procState, Bundle state, + PersistableBundle persistentState, List<ResultInfo> pendingResults, List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, - String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, - Bundle resumeArgs) { + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) { updateProcessState(procState, false); @@ -622,6 +622,7 @@ public final class ActivityThread { r.activityInfo = info; r.compatInfo = compatInfo; r.state = state; + r.persistentState = persistentState; r.pendingResults = pendingResults; r.pendingIntents = pendingNewIntents; @@ -632,7 +633,6 @@ public final class ActivityThread { r.profileFile = profileName; r.profileFd = profileFd; r.autoStopProfiler = autoStopProfiler; - r.activityOptions = resumeArgs; updatePendingConfiguration(curConfig); @@ -835,7 +835,7 @@ public final class ActivityThread { InetAddress.clearDnsCache(); } - public void setHttpProxy(String host, String port, String exclList, String pacFileUrl) { + public void setHttpProxy(String host, String port, String exclList, Uri pacFileUrl) { Proxy.setHttpProxySystemProperty(host, port, exclList, pacFileUrl); } @@ -1297,9 +1297,7 @@ public final class ActivityThread { break; case RESUME_ACTIVITY: Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityResume"); - final Pair<IBinder, Bundle> resumeArgs = (Pair<IBinder, Bundle>) msg.obj; - handleResumeActivity(resumeArgs.first, resumeArgs.second, true, - msg.arg1 != 0, true); + handleResumeActivity((IBinder) msg.obj, true, msg.arg1 != 0, true); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); break; case SEND_RESULT: @@ -2079,7 +2077,7 @@ public final class ActivityThread { + ", comp=" + name + ", token=" + token); } - return performLaunchActivity(r, null, null); + return performLaunchActivity(r, null); } public final Activity getActivity(IBinder token) { @@ -2132,8 +2130,7 @@ public final class ActivityThread { sendMessage(H.CLEAN_UP_CONTEXT, cci); } - private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent, - Bundle options) { + private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); ActivityInfo aInfo = r.activityInfo; @@ -2191,7 +2188,7 @@ public final class ActivityThread { + r.activityInfo.name + " with config " + config); activity.attach(appContext, this, getInstrumentation(), r.token, r.ident, app, r.intent, r.activityInfo, title, r.parent, - r.embeddedID, r.lastNonConfigurationInstances, config, options, + r.embeddedID, r.lastNonConfigurationInstances, config, r.voiceInteractor); if (customIntent != null) { @@ -2205,7 +2202,11 @@ public final class ActivityThread { } activity.mCalled = false; - mInstrumentation.callActivityOnCreate(activity, r.state); + if (r.isPersistable()) { + mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState); + } else { + mInstrumentation.callActivityOnCreate(activity, r.state); + } if (!activity.mCalled) { throw new SuperNotCalledException( "Activity " + r.intent.getComponent().toShortString() + @@ -2218,13 +2219,23 @@ public final class ActivityThread { r.stopped = false; } if (!r.activity.mFinished) { - if (r.state != null) { + if (r.isPersistable()) { + if (r.state != null || r.persistentState != null) { + mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state, + r.persistentState); + } + } else if (r.state != null) { mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state); } } if (!r.activity.mFinished) { activity.mCalled = false; - mInstrumentation.callActivityOnPostCreate(activity, r.state); + if (r.isPersistable()) { + mInstrumentation.callActivityOnPostCreate(activity, r.state, + r.persistentState); + } else { + mInstrumentation.callActivityOnPostCreate(activity, r.state); + } if (!activity.mCalled) { throw new SuperNotCalledException( "Activity " + r.intent.getComponent().toShortString() + @@ -2303,12 +2314,12 @@ public final class ActivityThread { if (localLOGV) Slog.v( TAG, "Handling launch of " + r); - Activity a = performLaunchActivity(r, customIntent, r.activityOptions); + Activity a = performLaunchActivity(r, customIntent); if (a != null) { r.createdConfig = new Configuration(mConfiguration); Bundle oldState = r.state; - handleResumeActivity(r.token, r.activityOptions, false, r.isForward, + handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed); if (!r.activity.mFinished && r.startsNotResumed) { @@ -2842,6 +2853,7 @@ public final class ActivityThread { r.paused = false; r.stopped = false; r.state = null; + r.persistentState = null; } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { throw new RuntimeException( @@ -2867,7 +2879,7 @@ public final class ActivityThread { r.mPendingRemoveWindowManager = null; } - final void handleResumeActivity(IBinder token, Bundle resumeArgs, + final void handleResumeActivity(IBinder token, 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. @@ -2998,19 +3010,10 @@ public final class ActivityThread { int h; if (w < 0) { Resources res = r.activity.getResources(); - 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); - } + int wId = com.android.internal.R.dimen.thumbnail_width; + int hId = com.android.internal.R.dimen.thumbnail_height; + mThumbnailWidth = w = res.getDimensionPixelSize(wId); + mThumbnailHeight = h = res.getDimensionPixelSize(hId); } else { h = mThumbnailHeight; } @@ -3069,7 +3072,7 @@ public final class ActivityThread { // Tell the activity manager we have paused. try { - ActivityManagerNative.getDefault().activityPaused(token); + ActivityManagerNative.getDefault().activityPaused(token, r.persistentState); } catch (RemoteException ex) { } } @@ -3099,17 +3102,13 @@ public final class ActivityThread { + r.intent.getComponent().toShortString()); Slog.e(TAG, e.getMessage(), e); } - Bundle state = null; if (finished) { r.activity.mFinished = true; } try { // Next have the activity save its current state and managed dialogs... if (!r.activity.mFinished && saveState) { - state = new Bundle(); - state.setAllowFds(false); - mInstrumentation.callActivityOnSaveInstanceState(r.activity, state); - r.state = state; + callCallActivityOnSaveInstanceState(r); } // Now we are idle. r.activity.mCalled = false; @@ -3145,7 +3144,7 @@ public final class ActivityThread { listeners.get(i).onPaused(r.activity); } - return state; + return !r.activity.mFinished && saveState ? r.state : null; } final void performStopActivity(IBinder token, boolean saveState) { @@ -3156,7 +3155,7 @@ public final class ActivityThread { private static class StopInfo implements Runnable { ActivityClientRecord activity; Bundle state; - Bitmap thumbnail; + PersistableBundle persistentState; CharSequence description; @Override public void run() { @@ -3164,7 +3163,7 @@ public final class ActivityThread { try { if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Reporting activity stopped: " + activity); ActivityManagerNative.getDefault().activityStopped( - activity.token, state, thumbnail, description); + activity.token, state, persistentState, description); } catch (RemoteException ex) { } } @@ -3203,7 +3202,6 @@ public final class ActivityThread { private void performStopActivityInner(ActivityClientRecord r, StopInfo info, boolean keepShown, boolean saveState) { if (localLOGV) Slog.v(TAG, "Performing stop of " + r); - Bundle state = null; if (r != null) { if (!keepShown && r.stopped) { if (r.activity.mFinished) { @@ -3223,7 +3221,6 @@ public final class ActivityThread { // First create a thumbnail for the activity... // For now, don't create the thumbnail here; we are // doing that by doing a screen snapshot. - info.thumbnail = null; //createThumbnailBitmap(r); info.description = r.activity.onCreateDescription(); } catch (Exception e) { if (!mInstrumentation.onException(r.activity, e)) { @@ -3238,12 +3235,7 @@ public final class ActivityThread { // Next have the activity save its current state and managed dialogs... if (!r.activity.mFinished && saveState) { if (r.state == null) { - state = new Bundle(); - state.setAllowFds(false); - mInstrumentation.callActivityOnSaveInstanceState(r.activity, state); - r.state = state; - } else { - state = r.state; + callCallActivityOnSaveInstanceState(r); } } @@ -3319,6 +3311,7 @@ public final class ActivityThread { // manager to proceed and allow us to go fully into the background. info.activity = r; info.state = r.state; + info.persistentState = r.persistentState; mH.post(info); } @@ -3775,9 +3768,7 @@ public final class ActivityThread { performPauseActivity(r.token, false, r.isPreHoneycomb()); } if (r.state == null && !r.stopped && !r.isPreHoneycomb()) { - r.state = new Bundle(); - r.state.setAllowFds(false); - mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state); + callCallActivityOnSaveInstanceState(r); } handleDestroyActivity(r.token, false, configChanges, true); @@ -3802,11 +3793,22 @@ public final class ActivityThread { } } r.startsNotResumed = tmp.startsNotResumed; - r.activityOptions = null; handleLaunchActivity(r, currentIntent); } + private void callCallActivityOnSaveInstanceState(ActivityClientRecord r) { + r.state = new Bundle(); + r.state.setAllowFds(false); + if (r.isPersistable()) { + r.persistentState = new PersistableBundle(); + mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state, + r.persistentState); + } else { + mInstrumentation.callActivityOnSaveInstanceState(r.activity, r.state); + } + } + ArrayList<ComponentCallbacks2> collectComponentCallbacks( boolean allActivities, Configuration newConfig) { ArrayList<ComponentCallbacks2> callbacks diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java index 3eb2fea..b739387 100644 --- a/core/java/android/app/ActivityTransitionCoordinator.java +++ b/core/java/android/app/ActivityTransitionCoordinator.java @@ -15,26 +15,22 @@ */ package android.app; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; import android.content.Context; +import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; 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.util.SparseArray; import android.view.View; import android.view.ViewGroup; -import android.view.ViewGroupOverlay; import android.view.ViewTreeObserver; import android.view.Window; import android.widget.ImageView; @@ -82,15 +78,14 @@ import java.util.Collection; * 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 + * Typical finishAfterTransition goes like this: + * 1) finishAfterTransition() creates an ExitTransitionCoordinator and calls startExit() + * - The Window start transitioning to Translucent with a new ActivityOptions. * - 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 + * 2) The ActivityOptions is received by the Activity and an EnterTransitionCoordinator is created. * - 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 @@ -98,21 +93,21 @@ import java.util.Collection; * 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 + * EnterTransitionCoordinator + * 6) MSG_TAKE_SHARED_ELEMENTS is received by 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. - * - MSG_HIDE_SHARED_ELEMENTS is sent to the EnterTransitionCoordinator - * 7) MSG_HIDE_SHARED_ELEMENTS is received by the EnterTransitionCoordinator + * - MSG_HIDE_SHARED_ELEMENTS is sent to the ExitTransitionCoordinator + * 7) MSG_HIDE_SHARED_ELEMENTS is received by the ExitTransitionCoordinator * - The shared elements are made INVISIBLE * 8) The exit transition completes in the finishing Activity. - * - MSG_EXIT_TRANSITION_COMPLETE is sent to the ExitTransitionCoordinator. + * - MSG_EXIT_TRANSITION_COMPLETE is sent to the EnterTransitionCoordinator. * - finish() is called on the exiting Activity - * 9) The MSG_EXIT_TRANSITION_COMPLETE is received by the ExitTransitionCoordinator. + * 9) 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. */ @@ -120,30 +115,24 @@ 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"; + static final String KEY_REMOTE_RECEIVER = "android:remoteReceiver"; + + protected static final String KEY_SCREEN_X = "shared_element:screenX"; + protected static final String KEY_SCREEN_Y = "shared_element:screenY"; + protected static final String KEY_TRANSLATION_Z = "shared_element:translationZ"; + protected static final String KEY_WIDTH = "shared_element:width"; + protected static final String KEY_HEIGHT = "shared_element:height"; + protected static final String KEY_BITMAP = "shared_element:bitmap"; + protected static final String KEY_SCALE_TYPE = "shared_element:scaleType"; + protected static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix"; - 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"; - private static final String KEY_SCALE_TYPE = "shared_element:scaleType"; - private static final String KEY_IMAGE_MATRIX = "shared_element:imageMatrix"; + // The background fade in/out duration. TODO: Enable tuning this. + public static final int FADE_BACKGROUND_DURATION_MS = 300; - private static final ImageView.ScaleType[] SCALE_TYPE_VALUES = ImageView.ScaleType.values(); + protected static final ImageView.ScaleType[] SCALE_TYPE_VALUES = ImageView.ScaleType.values(); /** * Sent by the exiting coordinator (either EnterTransitionCoordinator @@ -154,7 +143,7 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { * 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; + public static final int MSG_SET_REMOTE_RECEIVER = 100; /** * Sent by the entering coordinator to tell the exiting coordinator @@ -165,17 +154,10 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { 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; + public static final int MSG_ACTIVITY_STOPPED = 102; /** * Sent by the exiting coordinator (either EnterTransitionCoordinator @@ -186,7 +168,7 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { * 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; + public static final int MSG_TAKE_SHARED_ELEMENTS = 103; /** * Sent by the exiting coordinator (either @@ -196,348 +178,125 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { * remote coordinator. If it is false, it will trigger the enter * transition to start. */ - public static final int MSG_EXIT_TRANSITION_COMPLETE = 105; + public static final int MSG_EXIT_TRANSITION_COMPLETE = 104; /** * 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()); - } + public static final int MSG_START_EXIT_TRANSITION = 105; /** - * Called when MSG_PREPARE_RESTORE is called. This will only be received by - * ExitTransitionCoordinator. + * It took too long for a message from the entering Activity, so we canceled the transition. */ - protected void onPrepareRestore() { - mListener.onEnterReady(); - } + public static final int MSG_CANCEL = 106; /** - * 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. + * When returning, this is the destination location for the shared element. */ - protected void onRemoteSceneExitComplete() { - if (!allowOverlappingTransitions()) { - Transition transition = beginTransition(mEnteringViews, false, true, true); - onStartEnterTransition(transition, mEnteringViews); - } - mListener.onRemoteExitComplete(); - } + public static final int MSG_SHARED_ELEMENT_DESTINATION = 107; /** - * 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) + * Send the shared element positions. */ - 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); - ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageViewState = - setSharedElementState(state, accepted); - handleRejected(rejected); - - if (getViewsTransition() != null) { - setViewVisibility(mEnteringViews, View.INVISIBLE); - } - setViewVisibility(mSharedElements, View.VISIBLE); - Transition transition = beginTransition(mEnteringViews, true, allowOverlappingTransitions(), - true); - setOriginalImageViewState(originalImageViewState); - - 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) { + public static final int MSG_SEND_SHARED_ELEMENT_DESTINATION = 108; + + final private Window mWindow; + final protected ArrayList<String> mAllSharedElementNames; + final protected ArrayList<View> mSharedElements = new ArrayList<View>(); + final protected ArrayList<String> mSharedElementNames = new ArrayList<String>(); + final protected ArrayList<View> mTransitioningViews = new ArrayList<View>(); + final protected SharedElementListener mListener; + protected ResultReceiver mResultReceiver; + final private FixedEpicenterCallback mEpicenterCallback = new FixedEpicenterCallback(); + final protected boolean mIsReturning; + + public ActivityTransitionCoordinator(Window window, + ArrayList<String> allSharedElementNames, + ArrayList<String> accepted, ArrayList<String> localNames, + SharedElementListener listener, boolean isReturning) { + super(new Handler()); + mWindow = window; + mListener = listener; + mAllSharedElementNames = allSharedElementNames; + mIsReturning = isReturning; + setSharedElements(accepted, localNames); if (getViewsTransition() != null) { - setViewVisibility(enteringViews, View.VISIBLE); + getDecor().captureTransitioningViews(mTransitioningViews); + mTransitioningViews.removeAll(mSharedElements); } - mEnteringViews = null; - mListener.onStartEnterTransition(getSharedElementNames(), getSharedElements()); + setEpicenter(); } - /** - * 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(); + protected Window getWindow() { + return mWindow; } - /** - * 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(); + protected ViewGroup getDecor() { + return (mWindow == null) ? null : (ViewGroup) mWindow.getDecorView(); } /** - * 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. + * Sets the transition epicenter to the position of the first shared element. */ - 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 setEpicenter() { + View epicenter = null; + if (!mAllSharedElementNames.isEmpty() && !mSharedElementNames.isEmpty() && + mAllSharedElementNames.get(0).equals(mSharedElementNames.get(0))) { + epicenter = mSharedElements.get(0); } + setEpicenter(epicenter); } - protected void notifyExitTransitionComplete() { - if (!mNotifiedExitTransitionComplete) { - mNotifiedExitTransitionComplete = true; - mRemoteResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null); + private void setEpicenter(View view) { + if (view == null) { + mEpicenterCallback.setEpicenter(null); + } else { + 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(); + Rect epicenter = new Rect(left, top, right, bottom); + mEpicenterCallback.setEpicenter(epicenter); } } - protected void notifyPrepareRestore() { - mRemoteResultReceiver.send(MSG_PREPARE_RESTORE, null); - } - - protected void setRemoteResultReceiver(ResultReceiver resultReceiver) { - mRemoteResultReceiver = resultReceiver; + public ArrayList<String> getAcceptedNames() { + return mSharedElementNames; } - 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; + public ArrayList<String> getMappedNames() { + ArrayList<String> names = new ArrayList<String>(mSharedElements.size()); + for (int i = 0; i < mSharedElements.size(); i++) { + names.add(mSharedElements.get(i).getViewName()); + } + return names; } - 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); + public static void setViewVisibility(Collection<View> views, int visibility) { + if (views != null) { + for (View view : views) { + view.setVisibility(visibility); } } } - 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); + protected static Transition addTargets(Transition transition, Collection<View> views) { + if (transition == null || views == null || views.isEmpty()) { + return null; } - 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; + TransitionSet set = new TransitionSet(); + set.addTransition(transition); + if (views != null) { + for (View view: views) { + set.addTarget(view); + } } + return set; } - // private methods - - private Transition configureTransition(Transition transition) { + protected Transition configureTransition(Transition transition) { if (transition != null) { transition = transition.clone(); transition.setEpicenterCallback(mEpicenterCallback); @@ -545,46 +304,105 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { 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); + protected static Transition mergeTransitions(Transition transition1, Transition transition2) { + if (transition1 == null) { + return transition2; + } else if (transition2 == null) { + return transition1; + } else { + TransitionSet transitionSet = new TransitionSet(); + transitionSet.addTransition(transition1); + transitionSet.addTransition(transition2); + return transitionSet; + } + } + + private void setSharedElements(ArrayList<String> accepted, ArrayList<String> localNames) { + if (!mAllSharedElementNames.isEmpty()) { + ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); + getDecor().findNamedViews(sharedElements); + if (accepted != null) { + for (int i = 0; i < localNames.size(); i++) { + String localName = localNames.get(i); + String acceptedName = accepted.get(i); + if (!localName.equals(acceptedName)) { + View view = sharedElements.remove(localName); + if (view != null) { + sharedElements.put(acceptedName, view); + } + } + } + } + sharedElements.retainAll(mAllSharedElementNames); + mListener.remapSharedElements(mAllSharedElementNames, sharedElements); + sharedElements.retainAll(mAllSharedElementNames); + for (int i = 0; i < mAllSharedElementNames.size(); i++) { + String name = mAllSharedElementNames.get(i); + View sharedElement = sharedElements.get(name); + if (sharedElement != null) { + mSharedElementNames.add(name); + mSharedElements.add(sharedElement); } - targetIndex++; } } - for (int i = mSharedElements.size() - 1; i >= targetIndex; i--) { - mSharedElements.remove(i); - mTargetSharedNames.remove(i); + } + + protected void setResultReceiver(ResultReceiver resultReceiver) { + mResultReceiver = resultReceiver; + } + + protected abstract Transition getViewsTransition(); + + private static void setSharedElementState(View view, String name, Bundle transitionArgs, + int[] parentLoc) { + Bundle sharedElementBundle = transitionArgs.getBundle(name); + if (sharedElementBundle == null) { + return; } - Rect epicenter = null; - if (!mTargetSharedNames.isEmpty() - && mTargetSharedNames.get(0).equals(sharedElementNames.get(0))) { - epicenter = calcEpicenter(mSharedElements.get(0)); + + if (view instanceof ImageView) { + int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1); + if (scaleTypeInt >= 0) { + ImageView imageView = (ImageView) view; + ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt]; + imageView.setScaleType(scaleType); + if (scaleType == ImageView.ScaleType.MATRIX) { + float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX); + Matrix matrix = new Matrix(); + matrix.setValues(matrixValues); + imageView.setImageMatrix(matrix); + } + } } - mEpicenterCallback.setEpicenter(epicenter); + + 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); } - private ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> setSharedElementState( - Bundle sharedElementState, final ArrayList<View> acceptedOverlayViews) { + protected ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> setSharedElementState( + Bundle sharedElementState, final ArrayList<View> snapshots) { ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageState = new ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>>(); - final int[] tempLoc = new int[2]; if (sharedElementState != null) { - for (int i = 0; i < mSharedElements.size(); i++) { + int[] tempLoc = new int[2]; + for (int i = 0; i < mSharedElementNames.size(); i++) { View sharedElement = mSharedElements.get(i); - String name = mTargetSharedNames.get(i); + String name = mSharedElementNames.get(i); Pair<ImageView.ScaleType, Matrix> originalState = getOldImageState(sharedElement, name, sharedElementState); if (originalState != null) { @@ -593,20 +411,17 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { View parent = (View) sharedElement.getParent(); parent.getLocationOnScreen(tempLoc); setSharedElementState(sharedElement, name, sharedElementState, tempLoc); - sharedElement.requestLayout(); } } - mListener.onCaptureSharedElementStart(mTargetSharedNames, mSharedElements, - acceptedOverlayViews); + mListener.setSharedElementStart(mSharedElementNames, mSharedElements, snapshots); getDecor().getViewTreeObserver().addOnPreDrawListener( new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { getDecor().getViewTreeObserver().removeOnPreDrawListener(this); - mListener.onCaptureSharedElementEnd(mTargetSharedNames, mSharedElements, - acceptedOverlayViews); - mSharedElementTransitionStarted = true; + mListener.setSharedElementEnd(mSharedElementNames, mSharedElements, + snapshots); return true; } } @@ -620,6 +435,9 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { return null; } Bundle bundle = transitionArgs.getBundle(name); + if (bundle == null) { + return null; + } int scaleTypeInt = bundle.getInt(KEY_SCALE_TYPE, -1); if (scaleTypeInt < 0) { return null; @@ -636,65 +454,62 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { return Pair.create(originalScaleType, originalMatrix); } - /** - * 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; + protected ArrayList<View> createSnapshots(Bundle state, Collection<String> names) { + int numSharedElements = names.size(); + if (numSharedElements == 0) { + return null; } - - if (view instanceof ImageView) { - int scaleTypeInt = sharedElementBundle.getInt(KEY_SCALE_TYPE, -1); - if (scaleTypeInt >= 0) { - ImageView imageView = (ImageView) view; - ImageView.ScaleType scaleType = SCALE_TYPE_VALUES[scaleTypeInt]; - imageView.setScaleType(scaleType); - if (scaleType == ImageView.ScaleType.MATRIX) { - float[] matrixValues = sharedElementBundle.getFloatArray(KEY_IMAGE_MATRIX); - Matrix matrix = new Matrix(); - matrix.setValues(matrixValues); - imageView.setImageMatrix(matrix); + ArrayList<View> snapshots = new ArrayList<View>(numSharedElements); + Context context = getWindow().getContext(); + int[] parentLoc = new int[2]; + getDecor().getLocationOnScreen(parentLoc); + for (String name: names) { + Bundle sharedElementBundle = state.getBundle(name); + if (sharedElementBundle != null) { + Bitmap bitmap = sharedElementBundle.getParcelable(KEY_BITMAP); + View snapshot = new View(context); + Resources resources = getWindow().getContext().getResources(); + if (bitmap != null) { + snapshot.setBackground(new BitmapDrawable(resources, bitmap)); } + snapshot.setViewName(name); + setSharedElementState(snapshot, name, state, parentLoc); + snapshots.add(snapshot); } } + return snapshots; + } - 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); + protected static void setOriginalImageViewState( + ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalState) { + for (int i = 0; i < originalState.size(); i++) { + ImageView imageView = originalState.keyAt(i); + Pair<ImageView.ScaleType, Matrix> state = originalState.valueAt(i); + imageView.setScaleType(state.first); + imageView.setImageMatrix(state.second); + } + } - int left = x - parentLoc[0]; - int top = y - parentLoc[1]; - int right = left + width; - int bottom = top + height; - view.layout(left, top, right, bottom); + protected Bundle captureSharedElementState() { + Bundle bundle = new Bundle(); + int[] tempLoc = new int[2]; + for (int i = 0; i < mSharedElementNames.size(); i++) { + View sharedElement = mSharedElements.get(i); + String name = mSharedElementNames.get(i); + captureSharedElementState(sharedElement, name, bundle, tempLoc); + } + return bundle; } /** * 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 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[]) + * @param tempLoc A temporary int[2] for capturing the current location of views. */ private static void captureSharedElementState(View view, String name, Bundle transitionArgs, int[] tempLoc) { @@ -707,17 +522,17 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { float scaleY = view.getScaleY(); sharedElementBundle.putInt(KEY_SCREEN_Y, tempLoc[1]); - int height= Math.round(view.getHeight() * scaleY); + 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); + if (width > 0 && height > 0) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + view.draw(canvas); + sharedElementBundle.putParcelable(KEY_BITMAP, bitmap); + } if (view instanceof ImageView) { ImageView imageView = (ImageView) view; @@ -733,176 +548,6 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { 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 void setOriginalImageViewState( - ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalState) { - for (int i = 0; i < originalState.size(); i++) { - ImageView imageView = originalState.keyAt(i); - Pair<ImageView.ScaleType, Matrix> state = originalState.valueAt(i); - imageView.setScaleType(state.first); - imageView.setImageMatrix(state.second); - } - } - private static int scaleTypeToInt(ImageView.ScaleType scaleType) { for (int i = 0; i < SCALE_TYPE_VALUES.length; i++) { if (scaleType == SCALE_TYPE_VALUES[i]) { @@ -922,4 +567,5 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { return mEpicenter; } } + } diff --git a/core/java/android/app/ActivityTransitionState.java b/core/java/android/app/ActivityTransitionState.java new file mode 100644 index 0000000..b32e9ad --- /dev/null +++ b/core/java/android/app/ActivityTransitionState.java @@ -0,0 +1,209 @@ +/* + * 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.os.ResultReceiver; +import android.util.ArrayMap; +import android.view.View; +import android.view.Window; + +import java.util.ArrayList; + +/** + * This class contains all persistence-related functionality for Activity Transitions. + * Activities start exit and enter Activity Transitions through this class. + */ +class ActivityTransitionState { + + private static final String ENTERING_SHARED_ELEMENTS = "android:enteringSharedElements"; + + private static final String ENTERING_MAPPED_FROM = "android:enteringMappedFrom"; + + private static final String ENTERING_MAPPED_TO = "android:enteringMappedTo"; + + private static final String EXITING_MAPPED_FROM = "android:exitingMappedFrom"; + + private static final String EXITING_MAPPED_TO = "android:exitingMappedTo"; + + /** + * The shared elements that the calling Activity has said that they transferred to this + * Activity. + */ + private ArrayList<String> mEnteringNames; + + /** + * The shared elements that this Activity as accepted and mapped to local Views. + */ + private ArrayList<String> mEnteringFrom; + + /** + * The names of local Views that are mapped to those elements in mEnteringFrom. + */ + private ArrayList<String> mEnteringTo; + + /** + * The names of shared elements that were shared to the called Activity. + */ + private ArrayList<String> mExitingFrom; + + /** + * The names of local Views that were shared out, mapped to those elements in mExitingFrom. + */ + private ArrayList<String> mExitingTo; + + /** + * The ActivityOptions used to call an Activity. Used to make the elements restore + * Visibility of exited Views. + */ + private ActivityOptions mCalledActivityOptions; + + /** + * We must be able to cancel entering transitions to stop changing the Window to + * opaque when we exit before making the Window opaque. + */ + private EnterTransitionCoordinator mEnterTransitionCoordinator; + + /** + * ActivityOptions used on entering this Activity. + */ + private ActivityOptions mEnterActivityOptions; + + /** + * Has an exit transition been started? If so, we don't want to double-exit. + */ + private boolean mHasExited; + + public ActivityTransitionState() { + } + + public void readState(Bundle bundle) { + if (bundle != null) { + if (mEnterTransitionCoordinator == null || mEnterTransitionCoordinator.isReturning()) { + mEnteringNames = bundle.getStringArrayList(ENTERING_SHARED_ELEMENTS); + mEnteringFrom = bundle.getStringArrayList(ENTERING_MAPPED_FROM); + mEnteringTo = bundle.getStringArrayList(ENTERING_MAPPED_TO); + } + if (mEnterTransitionCoordinator == null) { + mExitingFrom = bundle.getStringArrayList(EXITING_MAPPED_FROM); + mExitingTo = bundle.getStringArrayList(EXITING_MAPPED_TO); + } + } + } + + public void saveState(Bundle bundle) { + if (mEnteringNames != null) { + bundle.putStringArrayList(ENTERING_SHARED_ELEMENTS, mEnteringNames); + bundle.putStringArrayList(ENTERING_MAPPED_FROM, mEnteringFrom); + bundle.putStringArrayList(ENTERING_MAPPED_TO, mEnteringTo); + } + if (mExitingFrom != null) { + bundle.putStringArrayList(EXITING_MAPPED_FROM, mExitingFrom); + bundle.putStringArrayList(EXITING_MAPPED_TO, mExitingTo); + } + } + + public void setEnterActivityOptions(Activity activity, ActivityOptions options) { + if (activity.getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS) + && options != null && mEnterActivityOptions == null + && options.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + mEnterActivityOptions = options; + if (mEnterActivityOptions.isReturning()) { + int result = mEnterActivityOptions.getResultCode(); + if (result != 0) { + activity.onActivityReenter(result, mEnterActivityOptions.getResultData()); + } + } + } + } + + public void enterReady(Activity activity) { + if (mEnterActivityOptions == null) { + return; + } + mHasExited = false; + ArrayList<String> sharedElementNames = mEnterActivityOptions.getSharedElementNames(); + ResultReceiver resultReceiver = mEnterActivityOptions.getResultReceiver(); + if (mEnterActivityOptions.isReturning()) { + restoreExitedViews(); + activity.getWindow().getDecorView().setVisibility(View.VISIBLE); + mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity, + resultReceiver, sharedElementNames, mExitingFrom, mExitingTo); + } else { + mEnterTransitionCoordinator = new EnterTransitionCoordinator(activity, + resultReceiver, sharedElementNames, null, null); + mEnteringNames = sharedElementNames; + mEnteringFrom = mEnterTransitionCoordinator.getAcceptedNames(); + mEnteringTo = mEnterTransitionCoordinator.getMappedNames(); + } + mExitingFrom = null; + mExitingTo = null; + mEnterActivityOptions = null; + } + + public void onStop() { + restoreExitedViews(); + if (mEnterTransitionCoordinator != null) { + mEnterTransitionCoordinator.stop(); + mEnterTransitionCoordinator = null; + } + } + + public void onResume() { + restoreExitedViews(); + } + + private void restoreExitedViews() { + if (mCalledActivityOptions != null) { + mCalledActivityOptions.dispatchActivityStopped(); + mCalledActivityOptions = null; + } + } + + public boolean startExitBackTransition(Activity activity) { + if (mEnteringNames == null) { + return false; + } else { + if (!mHasExited) { + mHasExited = true; + if (mEnterTransitionCoordinator != null) { + mEnterTransitionCoordinator.stop(); + mEnterTransitionCoordinator = null; + } + ArrayMap<String, View> sharedElements = new ArrayMap<String, View>(); + activity.getWindow().getDecorView().findNamedViews(sharedElements); + + ExitTransitionCoordinator exitCoordinator = + new ExitTransitionCoordinator(activity, mEnteringNames, mEnteringFrom, + mEnteringTo, true); + exitCoordinator.startExit(activity.mResultCode, activity.mResultData); + } + return true; + } + } + + public void startExitOutTransition(Activity activity, Bundle options) { + if (!activity.getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + return; + } + mCalledActivityOptions = new ActivityOptions(options); + if (mCalledActivityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { + mExitingFrom = mCalledActivityOptions.getSharedElementNames(); + mExitingTo = mCalledActivityOptions.getLocalSharedElementNames(); + mCalledActivityOptions.dispatchStartExit(); + } + } +} diff --git a/core/java/android/app/AlertDialog.java b/core/java/android/app/AlertDialog.java index ab148a9..4ce7835 100644 --- a/core/java/android/app/AlertDialog.java +++ b/core/java/android/app/AlertDialog.java @@ -92,6 +92,18 @@ public class AlertDialog extends Dialog implements DialogInterface { * the device's default alert theme with a light background. */ public static final int THEME_DEVICE_DEFAULT_LIGHT = 5; + + /** + * No layout hint. + * @hide + */ + public static final int LAYOUT_HINT_NONE = 0; + + /** + * Hint layout to the side. + * @hide + */ + public static final int LAYOUT_HINT_SIDE = 1; protected AlertDialog(Context context) { this(context, resolveDialogTheme(context, 0), true); @@ -208,6 +220,14 @@ public class AlertDialog extends Dialog implements DialogInterface { } /** + * Internal api to allow hinting for the best button panel layout. + * @hide + */ + void setButtonPanelLayoutHint(int layoutHint) { + mAlert.setButtonPanelLayoutHint(layoutHint); + } + + /** * Set a message to be sent when a button is pressed. * * @param whichButton Which button to set the message for, can be one of diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index d813dab..5867232 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -19,6 +19,7 @@ package android.app; import android.Manifest; import android.os.Binder; import android.os.IBinder; +import android.os.UserManager; import android.util.ArrayMap; import com.android.internal.app.IAppOpsService; import com.android.internal.app.IAppOpsCallback; @@ -412,6 +413,58 @@ public class AppOpsManager { }; /** + * Specifies whether an Op should be restricted by a user restriction. + * Each Op should be filled with a restriction string from UserManager or + * null to specify it is not affected by any user restriction. + */ + private static String[] sOpRestrictions = new String[] { + null, //COARSE_LOCATION + null, //FINE_LOCATION + null, //GPS + null, //VIBRATE + null, //READ_CONTACTS + null, //WRITE_CONTACTS + null, //READ_CALL_LOG + null, //WRITE_CALL_LOG + null, //READ_CALENDAR + null, //WRITE_CALENDAR + null, //WIFI_SCAN + null, //POST_NOTIFICATION + null, //NEIGHBORING_CELLS + null, //CALL_PHONE + null, //READ_SMS + null, //WRITE_SMS + null, //RECEIVE_SMS + null, //RECEIVE_EMERGECY_SMS + null, //RECEIVE_MMS + null, //RECEIVE_WAP_PUSH + null, //SEND_SMS + null, //READ_ICC_SMS + null, //WRITE_ICC_SMS + null, //WRITE_SETTINGS + null, //SYSTEM_ALERT_WINDOW + null, //ACCESS_NOTIFICATIONS + null, //CAMERA + null, //RECORD_AUDIO + null, //PLAY_AUDIO + null, //READ_CLIPBOARD + null, //WRITE_CLIPBOARD + null, //TAKE_MEDIA_BUTTONS + null, //TAKE_AUDIO_FOCUS + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MASTER_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_VOICE_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_RING_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_MEDIA_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ALARM_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_NOTIFICATION_VOLUME + UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_BLUETOOTH_VOLUME + null, //WAKE_LOCK + null, //MONITOR_LOCATION + null, //MONITOR_HIGH_POWER_LOCATION + null, //GET_USAGE_STATS + }; + + /** * This specifies the default mode for each operation. */ private static int[] sOpDefaultMode = new int[] { @@ -542,6 +595,10 @@ public class AppOpsManager { throw new IllegalStateException("sOpDisableReset length " + sOpDisableReset.length + " should be " + _NUM_OP); } + if (sOpRestrictions.length != _NUM_OP) { + throw new IllegalStateException("sOpRestrictions length " + sOpRestrictions.length + + " should be " + _NUM_OP); + } for (int i=0; i<_NUM_OP; i++) { if (sOpToString[i] != null) { sOpStrToOp.put(sOpToString[i], i); @@ -575,6 +632,14 @@ public class AppOpsManager { } /** + * Retrieve the user restriction associated with an operation, or null if there is not one. + * @hide + */ + public static String opToRestriction(int op) { + return sOpRestrictions[op]; + } + + /** * Retrieve the default mode for the operation. * @hide */ diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index efd3d86..84673d9 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -35,6 +35,7 @@ import android.content.pm.IPackageMoveObserver; import android.content.pm.IPackageStatsObserver; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.PermissionGroupInfo; @@ -1127,7 +1128,7 @@ final class ApplicationPackageManager extends PackageManager { public void installPackage(Uri packageURI, PackageInstallObserver observer, int flags, String installerPackageName) { try { - mPM.installPackageEtc(packageURI, null, observer.mObserver, + mPM.installPackageEtc(packageURI, null, observer.getBinder(), flags, installerPackageName); } catch (RemoteException e) { // Should never happen! @@ -1140,7 +1141,7 @@ final class ApplicationPackageManager extends PackageManager { Uri verificationURI, ManifestDigest manifestDigest, ContainerEncryptionParams encryptionParams) { try { - mPM.installPackageWithVerificationEtc(packageURI, null, observer.mObserver, flags, + mPM.installPackageWithVerificationEtc(packageURI, null, observer.getBinder(), flags, installerPackageName, verificationURI, manifestDigest, encryptionParams); } catch (RemoteException e) { // Should never happen! @@ -1153,7 +1154,7 @@ final class ApplicationPackageManager extends PackageManager { VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) { try { mPM.installPackageWithVerificationAndEncryptionEtc(packageURI, null, - observer.mObserver, flags, installerPackageName, verificationParams, + observer.getBinder(), flags, installerPackageName, verificationParams, encryptionParams); } catch (RemoteException e) { // Should never happen! @@ -1440,14 +1441,24 @@ final class ApplicationPackageManager extends PackageManager { return null; } + @Override + public PackageInstaller getPackageInstaller() { + try { + return new PackageInstaller(this, mPM.getPackageInstaller(), mContext.getUserId(), + mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + /** * @hide */ @Override - public void addForwardingIntentFilter(IntentFilter filter, boolean removable, int userIdOrig, - int userIdDest) { + public void addCrossProfileIntentFilter(IntentFilter filter, boolean removable, + int sourceUserId, int targetUserId) { try { - mPM.addForwardingIntentFilter(filter, removable, userIdOrig, userIdDest); + mPM.addCrossProfileIntentFilter(filter, removable, sourceUserId, targetUserId); } catch (RemoteException e) { // Should never happen! } @@ -1457,14 +1468,31 @@ final class ApplicationPackageManager extends PackageManager { * @hide */ @Override - public void clearForwardingIntentFilters(int userIdOrig) { + public void addForwardingIntentFilter(IntentFilter filter, boolean removable, int sourceUserId, + int targetUserId) { + addCrossProfileIntentFilter(filter, removable, sourceUserId, targetUserId); + } + + /** + * @hide + */ + @Override + public void clearCrossProfileIntentFilters(int sourceUserId) { try { - mPM.clearForwardingIntentFilters(userIdOrig); + mPM.clearCrossProfileIntentFilters(sourceUserId); } catch (RemoteException e) { // Should never happen! } } + /** + * @hide + */ + @Override + public void clearForwardingIntentFilters(int sourceUserId) { + clearCrossProfileIntentFilters(sourceUserId); + } + private final ContextImpl mContext; private final IPackageManager mPM; diff --git a/core/java/android/app/ApplicationThreadNative.java b/core/java/android/app/ApplicationThreadNative.java index 7f2fb59..ef4099f 100644 --- a/core/java/android/app/ApplicationThreadNative.java +++ b/core/java/android/app/ApplicationThreadNative.java @@ -25,10 +25,12 @@ import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.net.Uri; import android.os.Binder; import android.os.Bundle; import android.os.Debug; import android.os.Parcelable; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.IBinder; import android.os.Parcel; @@ -141,6 +143,7 @@ public abstract class ApplicationThreadNative extends Binder data.readStrongBinder()); int procState = data.readInt(); Bundle state = data.readBundle(); + PersistableBundle persistentState = data.readPersistableBundle(); List<ResultInfo> ri = data.createTypedArrayList(ResultInfo.CREATOR); List<Intent> pi = data.createTypedArrayList(Intent.CREATOR); boolean notResumed = data.readInt() != 0; @@ -149,11 +152,10 @@ public abstract class ApplicationThreadNative extends Binder ParcelFileDescriptor profileFd = data.readInt() != 0 ? ParcelFileDescriptor.CREATOR.createFromParcel(data) : null; boolean autoStopProfiler = data.readInt() != 0; - Bundle resumeArgs = data.readBundle(); scheduleLaunchActivity(intent, b, ident, info, curConfig, compatInfo, - voiceInteractor, procState, state, - ri, pi, notResumed, isForward, profileName, profileFd, autoStopProfiler, - resumeArgs); + voiceInteractor, procState, state, persistentState, + ri, pi, notResumed, isForward, profileName, profileFd, + autoStopProfiler); return true; } @@ -337,7 +339,7 @@ public abstract class ApplicationThreadNative extends Binder final String proxy = data.readString(); final String port = data.readString(); final String exclList = data.readString(); - final String pacFileUrl = data.readString(); + final Uri pacFileUrl = Uri.CREATOR.createFromParcel(data); setHttpProxy(proxy, port, exclList, pacFileUrl); return true; } @@ -731,11 +733,10 @@ 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, + IVoiceInteractor voiceInteractor, int procState, Bundle state, + PersistableBundle persistentState, List<ResultInfo> pendingResults, List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, - String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler, - Bundle resumeArgs) + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); @@ -748,6 +749,7 @@ class ApplicationThreadProxy implements IApplicationThread { data.writeStrongBinder(voiceInteractor != null ? voiceInteractor.asBinder() : null); data.writeInt(procState); data.writeBundle(state); + data.writePersistableBundle(persistentState); data.writeTypedList(pendingResults); data.writeTypedList(pendingNewIntents); data.writeInt(notResumed ? 1 : 0); @@ -760,7 +762,6 @@ 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(); @@ -1005,13 +1006,13 @@ class ApplicationThreadProxy implements IApplicationThread { } public void setHttpProxy(String proxy, String port, String exclList, - String pacFileUrl) throws RemoteException { + Uri pacFileUrl) throws RemoteException { Parcel data = Parcel.obtain(); data.writeInterfaceToken(IApplicationThread.descriptor); data.writeString(proxy); data.writeString(port); data.writeString(exclList); - data.writeString(pacFileUrl); + pacFileUrl.writeToParcel(data, 0); mRemote.transact(SET_HTTP_PROXY_TRANSACTION, data, null, IBinder.FLAG_ONEWAY); data.recycle(); } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 801182d..e03224c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -26,6 +26,7 @@ import com.android.internal.util.Preconditions; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; @@ -34,7 +35,9 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.IIntentReceiver; import android.content.IntentSender; +import android.content.IRestrictionsManager; import android.content.ReceiverCallNotAllowedException; +import android.content.RestrictionsManager; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; @@ -57,7 +60,9 @@ import android.hardware.ISerialManager; import android.hardware.SerialManager; import android.hardware.SystemSensorManager; import android.hardware.hdmi.HdmiCecManager; +import android.hardware.hdmi.HdmiControlManager; import android.hardware.hdmi.IHdmiCecService; +import android.hardware.hdmi.IHdmiControlService; import android.hardware.camera2.CameraManager; import android.hardware.display.DisplayManager; import android.hardware.input.InputManager; @@ -69,9 +74,13 @@ import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; import android.media.MediaRouter; -import android.media.session.SessionManager; +import android.media.session.MediaSessionManager; +import android.media.tv.ITvInputManager; +import android.media.tv.TvInputManager; import android.net.ConnectivityManager; import android.net.IConnectivityManager; +import android.net.EthernetManager; +import android.net.IEthernetManager; import android.net.INetworkPolicyManager; import android.net.NetworkPolicyManager; import android.net.NetworkScoreManager; @@ -80,8 +89,8 @@ 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.passpoint.IWifiPasspointManager; +import android.net.wifi.passpoint.WifiPasspointManager; import android.net.wifi.p2p.IWifiP2pManager; import android.net.wifi.p2p.WifiP2pManager; import android.nfc.NfcManager; @@ -112,8 +121,6 @@ import android.service.fingerprint.FingerprintManager; import android.service.fingerprint.FingerprintManagerReceiver; import android.service.fingerprint.FingerprintService; import android.telephony.TelephonyManager; -import android.tv.ITvInputManager; -import android.tv.TvInputManager; import android.content.ClipboardManager; import android.util.AndroidRuntimeException; import android.util.ArrayMap; @@ -130,10 +137,12 @@ import android.view.textservice.TextServicesManager; import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.app.admin.DevicePolicyManager; +import android.app.task.ITaskManager; import android.app.trust.TrustManager; import com.android.internal.annotations.GuardedBy; import com.android.internal.app.IAppOpsService; +import com.android.internal.appwidget.IAppWidgetService.Stub; import com.android.internal.os.IDropBoxManagerService; import java.io.File; @@ -244,6 +253,8 @@ class ContextImpl extends Context { private File[] mExternalFilesDirs; @GuardedBy("mSync") private File[] mExternalCacheDirs; + @GuardedBy("mSync") + private File[] mExternalMediaDirs; private static final String[] EMPTY_FILE_LIST = {}; @@ -381,6 +392,11 @@ class ContextImpl extends Context { return new HdmiCecManager(IHdmiCecService.Stub.asInterface(b)); }}); + registerService(HDMI_CONTROL_SERVICE, new StaticServiceFetcher() { + public Object createStaticService() { + IBinder b = ServiceManager.getService(HDMI_CONTROL_SERVICE); + return new HdmiControlManager(IHdmiControlService.Stub.asInterface(b)); + }}); registerService(CLIPBOARD_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { @@ -577,11 +593,11 @@ class ContextImpl extends Context { return new WifiManager(ctx.getOuterContext(), service); }}); - registerService(WIFI_HOTSPOT_SERVICE, new ServiceFetcher() { + registerService(WIFI_PASSPOINT_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); + IBinder b = ServiceManager.getService(WIFI_PASSPOINT_SERVICE); + IWifiPasspointManager service = IWifiPasspointManager.Stub.asInterface(b); + return new WifiPasspointManager(ctx.getOuterContext(), service); }}); registerService(WIFI_P2P_SERVICE, new ServiceFetcher() { @@ -598,6 +614,13 @@ class ContextImpl extends Context { return new WifiScanner(ctx.getOuterContext(), service); }}); + registerService(ETHERNET_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(ETHERNET_SERVICE); + IEthernetManager service = IEthernetManager.Stub.asInterface(b); + return new EthernetManager(ctx.getOuterContext(), service); + }}); + registerService(WINDOW_SERVICE, new ServiceFetcher() { Display mDefaultDisplay; public Object getService(ContextImpl ctx) { @@ -641,6 +664,13 @@ class ContextImpl extends Context { } }); + registerService(RESTRICTIONS_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(RESTRICTIONS_SERVICE); + IRestrictionsManager service = IRestrictionsManager.Stub.asInterface(b); + return new RestrictionsManager(ctx, service); + } + }); registerService(PRINT_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { IBinder iBinder = ServiceManager.getService(Context.PRINT_SERVICE); @@ -656,7 +686,7 @@ class ContextImpl extends Context { registerService(MEDIA_SESSION_SERVICE, new ServiceFetcher() { public Object createService(ContextImpl ctx) { - return new SessionManager(ctx); + return new MediaSessionManager(ctx); } }); registerService(TRUST_SERVICE, new ServiceFetcher() { @@ -683,6 +713,12 @@ class ContextImpl extends Context { public Object createService(ContextImpl ctx) { return new UsageStatsManager(ctx.getOuterContext()); }}); + + registerService(TASK_SERVICE, new ServiceFetcher() { + public Object createService(ContextImpl ctx) { + IBinder b = ServiceManager.getService(TASK_SERVICE); + return new TaskManagerImpl(ITaskManager.Stub.asInterface(b)); + }}); } static ContextImpl getImpl(Context context) { @@ -1014,6 +1050,18 @@ class ContextImpl extends Context { } @Override + public File[] getExternalMediaDirs() { + synchronized (mSync) { + if (mExternalMediaDirs == null) { + mExternalMediaDirs = Environment.buildExternalStorageAppMediaDirs(getPackageName()); + } + + // Create dirs if needed + return ensureDirsExistOrFilter(mExternalMediaDirs); + } + } + + @Override public File getFileStreamPath(String name) { return makeFilename(getFilesDir(), name); } @@ -1344,6 +1392,15 @@ class ContextImpl extends Context { public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { + sendOrderedBroadcastAsUser(intent, user, receiverPermission, AppOpsManager.OP_NONE, + resultReceiver, scheduler, initialCode, initialData, initialExtras); + } + + @Override + public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, + String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + Handler scheduler, + int initialCode, String initialData, Bundle initialExtras) { IIntentReceiver rd = null; if (resultReceiver != null) { if (mPackageInfo != null) { @@ -1367,7 +1424,7 @@ class ContextImpl extends Context { ActivityManagerNative.getDefault().broadcastIntent( mMainThread.getApplicationThread(), intent, resolvedType, rd, initialCode, initialData, initialExtras, receiverPermission, - AppOpsManager.OP_NONE, true, false, user.getIdentifier()); + appOp, true, false, user.getIdentifier()); } catch (RemoteException e) { } } @@ -1707,7 +1764,8 @@ class ContextImpl extends Context { arguments.setAllowFds(false); } return ActivityManagerNative.getDefault().startInstrumentation( - className, profileFile, 0, arguments, null, null, getUserId()); + className, profileFile, 0, arguments, null, null, getUserId(), + null /* ABI override */); } catch (RemoteException e) { // System has crashed, nothing we can do. } @@ -1818,8 +1876,8 @@ class ContextImpl extends Context { public void grantUriPermission(String toPackage, Uri uri, int modeFlags) { try { ActivityManagerNative.getDefault().grantUriPermission( - mMainThread.getApplicationThread(), toPackage, uri, - modeFlags); + mMainThread.getApplicationThread(), toPackage, + ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri)); } catch (RemoteException e) { } } @@ -1828,8 +1886,8 @@ class ContextImpl extends Context { public void revokeUriPermission(Uri uri, int modeFlags) { try { ActivityManagerNative.getDefault().revokeUriPermission( - mMainThread.getApplicationThread(), uri, - modeFlags); + mMainThread.getApplicationThread(), + ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri)); } catch (RemoteException e) { } } @@ -1838,12 +1896,17 @@ class ContextImpl extends Context { public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { try { return ActivityManagerNative.getDefault().checkUriPermission( - uri, pid, uid, modeFlags); + ContentProvider.getUriWithoutUserId(uri), pid, uid, modeFlags, + resolveUserId(uri)); } catch (RemoteException e) { return PackageManager.PERMISSION_DENIED; } } + private int resolveUserId(Uri uri) { + return ContentProvider.getUserIdFromUri(uri, getUserId()); + } + @Override public int checkCallingUriPermission(Uri uri, int modeFlags) { int pid = Binder.getCallingPid(); @@ -2280,12 +2343,16 @@ class ContextImpl extends Context { @Override protected IContentProvider acquireProvider(Context context, String auth) { - return mMainThread.acquireProvider(context, auth, mUser.getIdentifier(), true); + return mMainThread.acquireProvider(context, + ContentProvider.getAuthorityWithoutUserId(auth), + resolveUserIdFromAuthority(auth), true); } @Override protected IContentProvider acquireExistingProvider(Context context, String auth) { - return mMainThread.acquireExistingProvider(context, auth, mUser.getIdentifier(), true); + return mMainThread.acquireExistingProvider(context, + ContentProvider.getAuthorityWithoutUserId(auth), + resolveUserIdFromAuthority(auth), true); } @Override @@ -2295,7 +2362,9 @@ class ContextImpl extends Context { @Override protected IContentProvider acquireUnstableProvider(Context c, String auth) { - return mMainThread.acquireProvider(c, auth, mUser.getIdentifier(), false); + return mMainThread.acquireProvider(c, + ContentProvider.getAuthorityWithoutUserId(auth), + resolveUserIdFromAuthority(auth), false); } @Override @@ -2312,5 +2381,10 @@ class ContextImpl extends Context { public void appNotRespondingViaProvider(IContentProvider icp) { mMainThread.appNotRespondingViaProvider(icp.asBinder()); } + + /** @hide */ + protected int resolveUserIdFromAuthority(String auth) { + return ContentProvider.getUserIdFromAuthority(auth, mUser.getIdentifier()); + } } } diff --git a/core/java/android/app/DatePickerDialog.java b/core/java/android/app/DatePickerDialog.java index d168800..26c2c30 100644 --- a/core/java/android/app/DatePickerDialog.java +++ b/core/java/android/app/DatePickerDialog.java @@ -107,6 +107,7 @@ public class DatePickerDialog extends AlertDialog implements OnClickListener, (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.date_picker_dialog, null); setView(view); + setButtonPanelLayoutHint(LAYOUT_HINT_SIDE); mDatePicker = (DatePicker) view.findViewById(R.id.datePicker); mDatePicker.init(year, monthOfYear, dayOfMonth, this); updateTitle(year, monthOfYear, dayOfMonth); diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java index cbb8359..4b052e7 100644 --- a/core/java/android/app/EnterTransitionCoordinator.java +++ b/core/java/android/app/EnterTransitionCoordinator.java @@ -18,270 +18,341 @@ package android.app; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; -import android.graphics.drawable.ColorDrawable; +import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.os.ResultReceiver; import android.transition.Transition; +import android.transition.TransitionManager; +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; /** * 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. + * the enter scene and shared element transfer into the Scene, either during + * launch of an Activity or returning from a launched Activity. */ -class EnterTransitionCoordinator extends ActivityTransitionCoordinator - implements ViewTreeObserver.OnPreDrawListener { +class EnterTransitionCoordinator extends ActivityTransitionCoordinator { 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; + private static final long MAX_WAIT_MS = 1000; - /** - * 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 boolean mSharedElementTransitionStarted; 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()); + private boolean mHasStopped; + private Handler mHandler; + private boolean mIsCanceled; + private ObjectAnimator mBackgroundAnimator; + private boolean mIsExitTransitionComplete; + + public EnterTransitionCoordinator(Activity activity, ResultReceiver resultReceiver, + ArrayList<String> sharedElementNames, + ArrayList<String> acceptedNames, ArrayList<String> mappedNames) { + super(activity.getWindow(), sharedElementNames, acceptedNames, mappedNames, + getListener(activity, acceptedNames), acceptedNames != null); mActivity = activity; - setRemoteResultReceiver(resultReceiver); + setResultReceiver(resultReceiver); + prepareEnter(); + Bundle resultReceiverBundle = new Bundle(); + resultReceiverBundle.putParcelable(KEY_REMOTE_RECEIVER, this); + mResultReceiver.send(MSG_SET_REMOTE_RECEIVER, resultReceiverBundle); + if (mIsReturning) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + cancel(); + } + }; + mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS); + send(MSG_SEND_SHARED_ELEMENT_DESTINATION, null); + } } - 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(); + private void sendSharedElementDestination() { + ViewGroup decor = getDecor(); + if (!decor.isLayoutRequested()) { + Bundle state = captureSharedElementState(); + mResultReceiver.send(MSG_SHARED_ELEMENT_DESTINATION, state); + } else { + getDecor().getViewTreeObserver() + .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getDecor().getViewTreeObserver().removeOnPreDrawListener(this); + return true; } - } - }); - 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); + private static SharedElementListener getListener(Activity activity, + ArrayList<String> acceptedNames) { + boolean isReturning = acceptedNames != null; + return isReturning ? activity.mExitTransitionListener : activity.mEnterTransitionListener; } @Override - protected void sharedElementTransitionComplete(Bundle bundle) { - notifySharedElementTransitionComplete(bundle); - exitAfterSharedElementTransition(); + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case MSG_TAKE_SHARED_ELEMENTS: + if (!mIsCanceled) { + if (mHandler != null) { + mHandler.removeMessages(MSG_CANCEL); + } + onTakeSharedElements(resultData); + } + break; + case MSG_EXIT_TRANSITION_COMPLETE: + if (!mIsCanceled) { + mIsExitTransitionComplete = true; + if (mSharedElementTransitionStarted) { + onRemoteExitTransitionComplete(); + } + } + break; + case MSG_CANCEL: + cancel(); + break; + case MSG_SEND_SHARED_ELEMENT_DESTINATION: + sendSharedElementDestination(); + break; + } } - @Override - public boolean onPreDraw() { - getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this); - setEnteringViews(readyEnteringViews()); - notifySetListener(); - onPrepareRestore(); - return false; + private void cancel() { + if (!mIsCanceled) { + mIsCanceled = true; + if (getViewsTransition() == null) { + setViewVisibility(mSharedElements, View.VISIBLE); + } else { + mTransitioningViews.addAll(mSharedElements); + } + mSharedElementNames.clear(); + mSharedElements.clear(); + mAllSharedElementNames.clear(); + onTakeSharedElements(null); + onRemoteExitTransitionComplete(); + } } - @Override - public void startExit() { - if (!mExitTransitionStarted) { - mExitTransitionStarted = true; - startExitTransition(mEnteringSharedElementNames); + public boolean isReturning() { + return mIsReturning; + } + + protected void prepareEnter() { + setViewVisibility(mSharedElements, View.INVISIBLE); + if (getViewsTransition() != null) { + setViewVisibility(mTransitioningViews, View.INVISIBLE); + } + mActivity.overridePendingTransition(0, 0); + if (!mIsReturning) { + mActivity.convertToTranslucent(null, null); + Drawable background = getDecor().getBackground(); + if (background != null) { + getWindow().setBackgroundDrawable(null); + background = background.mutate(); + background.setAlpha(0); + getWindow().setBackgroundDrawable(background); + } + } else { + mActivity = null; // all done with it now. } } @Override protected Transition getViewsTransition() { - if (!mSupportsTransition) { - return null; + if (mIsReturning) { + return getWindow().getExitTransition(); + } else { + return getWindow().getEnterTransition(); } - return getWindow().getEnterTransition(); } - @Override protected Transition getSharedElementTransition() { - if (!mSupportsTransition) { - return null; + if (mIsReturning) { + return getWindow().getSharedElementExitTransition(); + } else { + return getWindow().getSharedElementEnterTransition(); } - 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(); - } - }); + protected void onTakeSharedElements(Bundle sharedElementState) { + setEpicenter(); + // Remove rejected shared elements + ArrayList<String> rejectedNames = new ArrayList<String>(mAllSharedElementNames); + rejectedNames.removeAll(mSharedElementNames); + ArrayList<View> rejectedSnapshots = createSnapshots(sharedElementState, rejectedNames); + mListener.handleRejectedSharedElements(rejectedSnapshots); + startRejectedAnimations(rejectedSnapshots); + + // Now start shared element transition + ArrayList<View> sharedElementSnapshots = createSnapshots(sharedElementState, + mSharedElementNames); + setViewVisibility(mSharedElements, View.VISIBLE); + ArrayMap<ImageView, Pair<ImageView.ScaleType, Matrix>> originalImageViewState = + setSharedElementState(sharedElementState, sharedElementSnapshots); + requestLayoutForSharedElements(); + + boolean startEnterTransition = allowOverlappingTransitions(); + boolean startSharedElementTransition = true; + Transition transition = beginTransition(startEnterTransition, startSharedElementTransition); + + if (startEnterTransition) { + startEnterTransition(transition); + } + + setOriginalImageViewState(originalImageViewState); + + if (mResultReceiver != null) { + mResultReceiver.send(MSG_HIDE_SHARED_ELEMENTS, null); } - super.onStartEnterTransition(transition, enteringViews); + mResultReceiver = null; // all done sending messages. } - public ArrayList<View> readyEnteringViews() { - ArrayList<View> enteringViews = new ArrayList<View>(); - getDecor().captureTransitioningViews(enteringViews); - if (getViewsTransition() != null) { - setViewVisibility(enteringViews, View.INVISIBLE); + private void requestLayoutForSharedElements() { + int numSharedElements = mSharedElements.size(); + for (int i = 0; i < numSharedElements; i++) { + mSharedElements.get(i).requestLayout(); } - return enteringViews; } - @Override - protected void startExitTransition(ArrayList<String> sharedElements) { - mMakeOpaque = false; - notifyPrepareRestore(); + private Transition beginTransition(boolean startEnterTransition, + boolean startSharedElementTransition) { + Transition sharedElementTransition = null; + if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { + sharedElementTransition = configureTransition(getSharedElementTransition()); + } + Transition viewsTransition = null; + if (startEnterTransition && !mTransitioningViews.isEmpty()) { + viewsTransition = configureTransition(getViewsTransition()); + viewsTransition = addTargets(viewsTransition, mTransitioningViews); + } - if (getDecor().getBackground() == null) { - ColorDrawable black = new ColorDrawable(0xFF000000); - getWindow().setBackgroundDrawable(black); + Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); + if (startSharedElementTransition) { + if (transition == null) { + sharedElementTransitionStarted(); + } else { + transition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionStart(Transition transition) { + transition.removeListener(this); + sharedElementTransitionStarted(); + } + }); + } } - if (mWasOpaque) { - mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { - @Override - public void onTranslucentConversionComplete(boolean drawComplete) { - fadeOutBackground(); - } - }); - } else { - fadeOutBackground(); + if (transition != null) { + TransitionManager.beginDelayedTransition(getDecor(), transition); + if (startSharedElementTransition && !mSharedElementNames.isEmpty()) { + mSharedElements.get(0).invalidate(); + } else if (startEnterTransition && !mTransitioningViews.isEmpty()) { + mTransitioningViews.get(0).invalidate(); + } } + return transition; + } - super.startExitTransition(sharedElements); + private void sharedElementTransitionStarted() { + mSharedElementTransitionStarted = true; + if (mIsExitTransitionComplete) { + send(MSG_EXIT_TRANSITION_COMPLETE, null); + } } - 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(); - } + private void startEnterTransition(Transition transition) { + setViewVisibility(mTransitioningViews, View.VISIBLE); + if (!mIsReturning) { + Drawable background = getDecor().getBackground(); + if (background != null) { + background = background.mutate(); + mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 255); + mBackgroundAnimator.setDuration(FADE_BACKGROUND_DURATION_MS); + mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + makeOpaque(); + } + }); + mBackgroundAnimator.start(); + } else if (transition != null) { + transition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + transition.removeListener(this); + makeOpaque(); + } + }); + } else { + makeOpaque(); } - }); - animator.setDuration(FADE_BACKGROUND_DURATION_MS); - animator.start(); + } } - @Override - protected void onExitTransitionEnd() { - mExitTransitionComplete = true; - exitAfterSharedElementTransition(); - super.onExitTransitionEnd(); + public void stop() { + mHasStopped = true; + mActivity = null; + mIsCanceled = true; + mResultReceiver = null; + if (mBackgroundAnimator != null) { + mBackgroundAnimator.cancel(); + mBackgroundAnimator = null; + } } - @Override - protected void onSharedElementTransitionEnd() { - mSharedElementTransitionComplete = true; - if (mBackgroundFadedOut) { - super.onSharedElementTransitionEnd(); + private void makeOpaque() { + if (!mHasStopped) { + mActivity.convertFromTranslucent(); + mActivity = null; } } - @Override - protected boolean allowOverlappingTransitions() { - return getWindow().getAllowEnterTransitionOverlap(); + private boolean allowOverlappingTransitions() { + return mIsReturning ? getWindow().getAllowExitTransitionOverlap() + : getWindow().getAllowEnterTransitionOverlap(); } - private void exitAfterSharedElementTransition() { - if (mSharedElementTransitionComplete && mExitTransitionComplete && mBackgroundFadedOut) { - mActivity.finish(); - if (mSupportsTransition) { - mActivity.overridePendingTransition(0, 0); + private void startRejectedAnimations(final ArrayList<View> rejectedSnapshots) { + if (rejectedSnapshots == null || rejectedSnapshots.isEmpty()) { + return; + } + ViewGroupOverlay overlay = getDecor().getOverlay(); + ObjectAnimator animator = null; + int numRejected = rejectedSnapshots.size(); + for (int i = 0; i < numRejected; i++) { + View snapshot = rejectedSnapshots.get(i); + overlay.add(snapshot); + animator = ObjectAnimator.ofFloat(snapshot, View.ALPHA, 1, 0); + animator.start(); + } + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + ViewGroupOverlay overlay = getDecor().getOverlay(); + int numRejected = rejectedSnapshots.size(); + for (int i = 0; i < numRejected; i++) { + overlay.remove(rejectedSnapshots.get(i)); + } } - notifyExitTransitionComplete(); - clearConnections(); + }); + } + + protected void onRemoteExitTransitionComplete() { + if (!allowOverlappingTransitions()) { + boolean startEnterTransition = true; + boolean startSharedElementTransition = false; + Transition transition = beginTransition(startEnterTransition, + startSharedElementTransition); + startEnterTransition(transition); } } } diff --git a/core/java/android/app/ExitTransitionCoordinator.java b/core/java/android/app/ExitTransitionCoordinator.java index d920787..ba1638f 100644 --- a/core/java/android/app/ExitTransitionCoordinator.java +++ b/core/java/android/app/ExitTransitionCoordinator.java @@ -15,11 +15,19 @@ */ package android.app; +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.ObjectAnimator; +import android.content.Intent; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.Handler; +import android.os.Message; import android.transition.Transition; -import android.util.Pair; +import android.transition.TransitionManager; import android.view.View; -import android.view.Window; +import android.view.ViewTreeObserver; import java.util.ArrayList; @@ -30,142 +38,287 @@ import java.util.ArrayList; */ class ExitTransitionCoordinator extends ActivityTransitionCoordinator { private static final String TAG = "ExitTransitionCoordinator"; + private static final long MAX_WAIT_MS = 1000; - /** - * 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; + private Bundle mSharedElementBundle; - /** - * Has the shared element transition completed? - */ - private boolean mSharedElementsComplete; + private boolean mExitNotified; - public ExitTransitionCoordinator(Window window, - ActivityOptions.ActivityTransitionListener listener) { - super(window); - setActivityTransitionListener(listener); - } + private boolean mSharedElementNotified; - @Override - protected void onSetResultReceiver() { - mIsResultReceiverSet = true; - notifyCompletions(); + private Activity mActivity; + + private boolean mIsBackgroundReady; + + private boolean mIsCanceled; + + private Handler mHandler; + + private ObjectAnimator mBackgroundAnimator; + + private boolean mIsHidden; + + private boolean mExitTransitionStarted; + + private Bundle mExitSharedElementBundle; + + public ExitTransitionCoordinator(Activity activity, ArrayList<String> names, + ArrayList<String> accepted, ArrayList<String> mapped, boolean isReturning) { + super(activity.getWindow(), names, accepted, mapped, getListener(activity, isReturning), + isReturning); + mIsBackgroundReady = !isReturning; + mActivity = activity; } - @Override - protected void onPrepareRestore() { - makeTransitioningViewsInvisible(); - setEnteringViews(mTransitioningViews); - mTransitioningViews = null; - super.onPrepareRestore(); + private static SharedElementListener getListener(Activity activity, boolean isReturning) { + return isReturning ? activity.mEnterTransitionListener : activity.mExitTransitionListener; } @Override - protected void onTakeSharedElements(ArrayList<String> sharedElementNames, Bundle state) { - super.onTakeSharedElements(sharedElementNames, state); - clearConnections(); + protected void onReceiveResult(int resultCode, Bundle resultData) { + switch (resultCode) { + case MSG_SET_REMOTE_RECEIVER: + mResultReceiver = resultData.getParcelable(KEY_REMOTE_RECEIVER); + if (mIsCanceled) { + mResultReceiver.send(MSG_CANCEL, null); + mResultReceiver = null; + } else { + if (mHandler != null) { + mHandler.removeMessages(MSG_CANCEL); + } + notifyComplete(); + } + break; + case MSG_HIDE_SHARED_ELEMENTS: + if (!mIsCanceled) { + hideSharedElements(); + } + break; + case MSG_START_EXIT_TRANSITION: + startExit(); + break; + case MSG_ACTIVITY_STOPPED: + setViewVisibility(mTransitioningViews, View.VISIBLE); + setViewVisibility(mSharedElements, View.VISIBLE); + mIsHidden = true; + break; + case MSG_SHARED_ELEMENT_DESTINATION: + mExitSharedElementBundle = resultData; + if (mExitTransitionStarted) { + startSharedElementExit(); + } + break; + } } - @Override - protected void onActivityStopped() { - if (getViewsTransition() != null) { - setViewVisibility(mTransitioningViews, View.VISIBLE); + private void startSharedElementExit() { + if (!mSharedElements.isEmpty() && getSharedElementTransition() != null) { + Transition transition = getSharedElementExitTransition(); + TransitionManager.beginDelayedTransition(getDecor(), transition); + ArrayList<View> sharedElementSnapshots = createSnapshots(mExitSharedElementBundle, + mSharedElementNames); + setSharedElementState(mExitSharedElementBundle, sharedElementSnapshots); } - super.onActivityStopped(); } - @Override - protected void sharedElementTransitionComplete(Bundle bundle) { - mSharedElements = bundle; - mSharedElementsComplete = true; - notifyCompletions(); + private void hideSharedElements() { + setViewVisibility(mSharedElements, View.INVISIBLE); + finishIfNecessary(); } - @Override - protected void onExitTransitionEnd() { - mExitComplete = true; - notifyCompletions(); - super.onExitTransitionEnd(); + public void startExit() { + beginTransitions(); + setViewVisibility(mTransitioningViews, View.INVISIBLE); } - private void notifyCompletions() { - if (mIsResultReceiverSet && mSharedElementsComplete) { - if (mSharedElements != null) { - notifySharedElementTransitionComplete(mSharedElements); - mSharedElements = null; + public void startExit(int resultCode, Intent data) { + mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + mIsCanceled = true; + mActivity.finish(); + mActivity = null; } - if (mExitComplete) { - notifyExitTransitionComplete(); + }; + mHandler.sendEmptyMessageDelayed(MSG_CANCEL, MAX_WAIT_MS); + if (getDecor().getBackground() == null) { + ColorDrawable black = new ColorDrawable(0xFF000000); + black.setAlpha(0); + getWindow().setBackgroundDrawable(black); + black.setAlpha(255); + } + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(mActivity, this, + mAllSharedElementNames, resultCode, data); + mActivity.convertToTranslucent(new Activity.TranslucentConversionListener() { + @Override + public void onTranslucentConversionComplete(boolean drawComplete) { + if (!mIsCanceled) { + fadeOutBackground(); + } } + }, options); + Transition sharedElementTransition = mSharedElements.isEmpty() + ? null : getSharedElementTransition(); + if (sharedElementTransition == null) { + sharedElementTransitionComplete(); + } + Transition transition = mergeTransitions(sharedElementTransition, getExitTransition()); + if (transition == null) { + mExitTransitionStarted = true; + } else { + TransitionManager.beginDelayedTransition(getDecor(), transition); + setViewVisibility(mTransitioningViews, View.INVISIBLE); + getDecor().getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getDecor().getViewTreeObserver().removeOnPreDrawListener(this); + mExitTransitionStarted = true; + if (mExitSharedElementBundle != null) { + startSharedElementExit(); + } + notifyComplete(); + return true; + } + }); } } - @Override - public void startExit() { - if (!mExitStarted) { - mExitStarted = true; - setSharedElements(); - startExitTransition(getSharedElementNames()); + private void fadeOutBackground() { + if (mBackgroundAnimator == null) { + Drawable background = getDecor().getBackground(); + mBackgroundAnimator = ObjectAnimator.ofInt(background, "alpha", 0); + mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mBackgroundAnimator = null; + if (!mIsCanceled) { + mIsBackgroundReady = true; + notifyComplete(); + } + } + }); + mBackgroundAnimator.setDuration(FADE_BACKGROUND_DURATION_MS); + mBackgroundAnimator.start(); } } - @Override - protected Transition getViewsTransition() { - if (!getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { - return null; + private Transition getExitTransition() { + Transition viewsTransition = null; + if (!mTransitioningViews.isEmpty()) { + viewsTransition = configureTransition(getViewsTransition()); + } + if (viewsTransition == null) { + exitTransitionComplete(); + } else { + viewsTransition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + exitTransitionComplete(); + if (mIsHidden) { + setViewVisibility(mTransitioningViews, View.VISIBLE); + } + } + + @Override + public void onTransitionCancel(Transition transition) { + super.onTransitionCancel(transition); + } + }); } - return getWindow().getExitTransition(); + return viewsTransition; } - @Override - protected Transition getSharedElementTransition() { - if (!getWindow().hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { - return null; + private Transition getSharedElementExitTransition() { + Transition sharedElementTransition = null; + if (!mSharedElements.isEmpty()) { + sharedElementTransition = configureTransition(getSharedElementTransition()); } - return getWindow().getSharedElementExitTransition(); + if (sharedElementTransition == null) { + sharedElementTransitionComplete(); + } else { + sharedElementTransition.addListener(new Transition.TransitionListenerAdapter() { + @Override + public void onTransitionEnd(Transition transition) { + sharedElementTransitionComplete(); + if (mIsHidden) { + setViewVisibility(mSharedElements, View.VISIBLE); + } + } + }); + mSharedElements.get(0).invalidate(); + } + return sharedElementTransition; } - private void makeTransitioningViewsInvisible() { - if (getViewsTransition() != null) { - setViewVisibility(mTransitioningViews, View.INVISIBLE); + private void beginTransitions() { + Transition sharedElementTransition = getSharedElementExitTransition(); + Transition viewsTransition = getExitTransition(); + + Transition transition = mergeTransitions(sharedElementTransition, viewsTransition); + mExitTransitionStarted = true; + if (transition != null) { + TransitionManager.beginDelayedTransition(getDecor(), transition); } } - @Override - protected void onStartExitTransition(ArrayList<View> exitingViews) { - mTransitioningViews = new ArrayList<View>(); - if (exitingViews != null) { - mTransitioningViews.addAll(exitingViews); + private void exitTransitionComplete() { + mExitComplete = true; + notifyComplete(); + } + + protected boolean isReadyToNotify() { + return mSharedElementBundle != null && mResultReceiver != null && mIsBackgroundReady + && mExitTransitionStarted; + } + + private void sharedElementTransitionComplete() { + mSharedElementBundle = captureSharedElementState(); + notifyComplete(); + } + + protected void notifyComplete() { + if (isReadyToNotify()) { + if (!mSharedElementNotified) { + mSharedElementNotified = true; + mResultReceiver.send(MSG_TAKE_SHARED_ELEMENTS, mSharedElementBundle); + } + if (!mExitNotified && mExitComplete) { + mExitNotified = true; + mResultReceiver.send(MSG_EXIT_TRANSITION_COMPLETE, null); + mResultReceiver = null; // done talking + finishIfNecessary(); + } + } + } + + private void finishIfNecessary() { + if (mIsReturning && mExitNotified && mActivity != null && (mSharedElements.isEmpty() + || mSharedElements.get(0).getVisibility() == View.INVISIBLE)) { + mActivity.finish(); + mActivity.overridePendingTransition(0, 0); + mActivity = null; + } + if (!mIsReturning && mExitNotified) { + mActivity = null; // don't need it anymore } - mTransitioningViews.addAll(getSharedElements()); } @Override - protected boolean allowOverlappingTransitions() { - return getWindow().getAllowExitTransitionOverlap(); + protected Transition getViewsTransition() { + if (mIsReturning) { + return getWindow().getEnterTransition(); + } else { + return getWindow().getExitTransition(); + } + } + + protected Transition getSharedElementTransition() { + if (mIsReturning) { + return getWindow().getSharedElementEnterTransition(); + } else { + return getWindow().getSharedElementExitTransition(); + } } } diff --git a/core/java/android/app/FragmentBreadCrumbs.java b/core/java/android/app/FragmentBreadCrumbs.java index e4de7af..ab0fc66 100644 --- a/core/java/android/app/FragmentBreadCrumbs.java +++ b/core/java/android/app/FragmentBreadCrumbs.java @@ -37,7 +37,10 @@ import android.widget.TextView; * * <p>The default style for this view is * {@link android.R.style#Widget_FragmentBreadCrumbs}. + * + * @deprecated This widget is no longer supported. */ +@Deprecated public class FragmentBreadCrumbs extends ViewGroup implements FragmentManager.OnBackStackChangedListener { Activity mActivity; @@ -88,6 +91,9 @@ public class FragmentBreadCrumbs extends ViewGroup this(context, attrs, defStyleAttr, 0); } + /** + * @hide + */ public FragmentBreadCrumbs( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index 2e9cdf3b7..bf2d7e5 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -45,6 +45,7 @@ import android.os.IInterface; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.Parcelable; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.StrictMode; import android.service.voice.IVoiceInteractionSession; @@ -104,13 +105,14 @@ public interface IActivityManager extends IInterface { public void activityResumed(IBinder token) throws RemoteException; public void activityIdle(IBinder token, Configuration config, boolean stopProfiling) throws RemoteException; - public void activityPaused(IBinder token) throws RemoteException; + public void activityPaused(IBinder token, PersistableBundle persistentState) throws RemoteException; public void activityStopped(IBinder token, Bundle state, - Bitmap thumbnail, CharSequence description) throws RemoteException; + PersistableBundle persistentState, CharSequence description) throws RemoteException; public void activitySlept(IBinder token) throws RemoteException; public void activityDestroyed(IBinder token) throws RemoteException; public String getCallingPackage(IBinder token) throws RemoteException; public ComponentName getCallingActivity(IBinder token) throws RemoteException; + public List<IAppTask> getAppTasks() throws RemoteException; public List<RunningTaskInfo> getTasks(int maxNum, int flags) throws RemoteException; public List<ActivityManager.RecentTaskInfo> getRecentTasks(int maxNum, int flags, int userId) throws RemoteException; @@ -174,7 +176,8 @@ public interface IActivityManager extends IInterface { public boolean startInstrumentation(ComponentName className, String profileFile, int flags, Bundle arguments, IInstrumentationWatcher watcher, - IUiAutomationConnection connection, int userId) throws RemoteException; + IUiAutomationConnection connection, int userId, + String abiOverride) throws RemoteException; public void finishInstrumentation(IApplicationThread target, int resultCode, Bundle results) throws RemoteException; @@ -209,14 +212,16 @@ public interface IActivityManager extends IInterface { public int checkPermission(String permission, int pid, int uid) throws RemoteException; - public int checkUriPermission(Uri uri, int pid, int uid, int mode) + public int checkUriPermission(Uri uri, int pid, int uid, int mode, int userId) + throws RemoteException; + public void grantUriPermission(IApplicationThread caller, String targetPkg, Uri uri, + int mode, int userId) throws RemoteException; + public void revokeUriPermission(IApplicationThread caller, Uri uri, int mode, int userId) + throws RemoteException; + public void takePersistableUriPermission(Uri uri, int modeFlags, int userId) + throws RemoteException; + public void releasePersistableUriPermission(Uri uri, int modeFlags, int userId) throws RemoteException; - public void grantUriPermission(IApplicationThread caller, String targetPkg, - Uri uri, int mode) throws RemoteException; - public void revokeUriPermission(IApplicationThread caller, Uri uri, - int mode) throws RemoteException; - public void takePersistableUriPermission(Uri uri, int modeFlags) throws RemoteException; - public void releasePersistableUriPermission(Uri uri, int modeFlags) throws RemoteException; public ParceledListSlice<UriPermission> getPersistedUriPermissions( String packageName, boolean incoming) throws RemoteException; @@ -308,8 +313,9 @@ public interface IActivityManager extends IInterface { public void finishHeavyWeightApp() throws RemoteException; public boolean convertFromTranslucent(IBinder token) throws RemoteException; - public boolean convertToTranslucent(IBinder token) throws RemoteException; + public boolean convertToTranslucent(IBinder token, ActivityOptions options) throws RemoteException; public void notifyActivityDrawn(IBinder token) throws RemoteException; + public ActivityOptions getActivityOptions(IBinder token) throws RemoteException; public void setImmersive(IBinder token, boolean immersive) throws RemoteException; public boolean isImmersive(IBinder token) throws RemoteException; @@ -322,12 +328,12 @@ public interface IActivityManager extends IInterface { public IBinder newUriPermissionOwner(String name) throws RemoteException; public void grantUriPermissionFromOwner(IBinder owner, int fromUid, String targetPkg, - Uri uri, int mode) throws RemoteException; + Uri uri, int mode, int userId) throws RemoteException; public void revokeUriPermissionFromOwner(IBinder owner, Uri uri, - int mode) throws RemoteException; + int mode, int userId) throws RemoteException; - public int checkGrantUriPermission(int callingUid, String targetPkg, - Uri uri, int modeFlags) throws RemoteException; + public int checkGrantUriPermission(int callingUid, String targetPkg, Uri uri, + int modeFlags, int userId) throws RemoteException; // Cause the specified process to dump the specified heap. public boolean dumpHeap(String process, int userId, boolean managed, String path, @@ -435,7 +441,7 @@ public interface IActivityManager extends IInterface { public boolean isInLockTaskMode() throws RemoteException; /** @hide */ - public void setRecentsActivityValues(IBinder token, ActivityManager.RecentsActivityValues values) + public void setTaskDescription(IBinder token, ActivityManager.TaskDescription values) throws RemoteException; /* @@ -734,6 +740,8 @@ public interface IActivityManager extends IInterface { 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_RECENTS_ACTIVITY_VALUES_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+217; + int SET_TASK_DESCRIPTION_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+217; int START_VOICE_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+218; + int GET_ACTIVITY_OPTIONS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+219; + int GET_APP_TASKS_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+220; } diff --git a/core/java/android/app/IAppTask.aidl b/core/java/android/app/IAppTask.aidl new file mode 100644 index 0000000..268b4dd --- /dev/null +++ b/core/java/android/app/IAppTask.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.app; + +import android.app.ActivityManager; + +/** @hide */ +interface IAppTask { + void finishAndRemoveTask(); + ActivityManager.RecentTaskInfo getTaskInfo(); +} diff --git a/core/java/android/app/IApplicationThread.java b/core/java/android/app/IApplicationThread.java index fefba8a..d0df7c3 100644 --- a/core/java/android/app/IApplicationThread.java +++ b/core/java/android/app/IApplicationThread.java @@ -25,9 +25,11 @@ import android.content.pm.ProviderInfo; import android.content.pm.ServiceInfo; import android.content.res.CompatibilityInfo; import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; import android.os.Debug; import android.os.ParcelFileDescriptor; +import android.os.PersistableBundle; import android.os.RemoteException; import android.os.IBinder; import android.os.IInterface; @@ -58,10 +60,9 @@ public interface IApplicationThread extends IInterface { 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, - Bundle resumeArgs) + PersistableBundle persistentState, List<ResultInfo> pendingResults, + List<Intent> pendingNewIntents, boolean notResumed, boolean isForward, + String profileName, ParcelFileDescriptor profileFd, boolean autoStopProfiler) throws RemoteException; void scheduleRelaunchActivity(IBinder token, List<ResultInfo> pendingResults, List<Intent> pendingNewIntents, int configChanges, @@ -105,7 +106,7 @@ public interface IApplicationThread extends IInterface { void updateTimeZone() throws RemoteException; void clearDnsCache() throws RemoteException; void setHttpProxy(String proxy, String port, String exclList, - String pacFileUrl) throws RemoteException; + Uri pacFileUrl) throws RemoteException; void processInBackground() throws RemoteException; void dumpService(FileDescriptor fd, IBinder servicetoken, String[] args) throws RemoteException; diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index b917263..a3ffc00 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -21,6 +21,7 @@ import android.app.ITransientNotification; import android.app.Notification; import android.content.ComponentName; import android.content.Intent; +import android.content.pm.ParceledListSlice; import android.net.Uri; import android.service.notification.Condition; import android.service.notification.IConditionListener; @@ -43,6 +44,8 @@ interface INotificationManager void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled); boolean areNotificationsEnabledForPackage(String pkg, int uid); + // TODO: Remove this when callers have been migrated to the equivalent + // INotificationListener method. StatusBarNotification[] getActiveNotifications(String callingPkg); StatusBarNotification[] getHistoricalNotifications(String callingPkg, int count); @@ -52,8 +55,7 @@ interface INotificationManager void cancelNotificationFromListener(in INotificationListener token, String pkg, String tag, int id); void cancelNotificationsFromListener(in INotificationListener token, in String[] keys); - StatusBarNotification[] getActiveNotificationsFromListener(in INotificationListener token, in String[] keys); - String[] getActiveNotificationKeysFromListener(in INotificationListener token); + ParceledListSlice getActiveNotificationsFromListener(in INotificationListener token); ZenModeConfig getZenModeConfig(); boolean setZenModeConfig(in ZenModeConfig config); diff --git a/core/java/android/app/IUiAutomationConnection.aidl b/core/java/android/app/IUiAutomationConnection.aidl index 347de97..8ab9ac3 100644 --- a/core/java/android/app/IUiAutomationConnection.aidl +++ b/core/java/android/app/IUiAutomationConnection.aidl @@ -43,4 +43,5 @@ interface IUiAutomationConnection { WindowContentFrameStats getWindowContentFrameStats(int windowId); void clearWindowAnimationFrameStats(); WindowAnimationFrameStats getWindowAnimationFrameStats(); + void executeShellCommand(String command, in ParcelFileDescriptor fd); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index e58ccb8..b78b9c9 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -30,6 +30,7 @@ import android.os.IBinder; import android.os.Looper; import android.os.MessageQueue; import android.os.PerformanceCollector; +import android.os.PersistableBundle; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -1035,10 +1036,10 @@ public class Instrumentation { IllegalAccessException { Activity activity = (Activity)clazz.newInstance(); ActivityThread aThread = null; - activity.attach(context, aThread, this, token, application, intent, + activity.attach(context, aThread, this, token, 0, application, intent, info, title, parent, id, (Activity.NonConfigurationInstances)lastNonConfigurationInstance, - new Configuration()); + new Configuration(), null); return activity; } @@ -1061,15 +1062,7 @@ public class Instrumentation { return (Activity)cl.loadClass(className).newInstance(); } - /** - * Perform calling of an activity's {@link Activity#onCreate} - * method. The default implementation simply calls through to that method. - * - * @param activity The activity being created. - * @param icicle The previously frozen state (or null) to pass through to - * onCreate(). - */ - public void callActivityOnCreate(Activity activity, Bundle icicle) { + private void prePerformCreate(Activity activity) { if (mWaitingActivities != null) { synchronized (mSync) { final int N = mWaitingActivities.size(); @@ -1083,9 +1076,9 @@ public class Instrumentation { } } } - - activity.performCreate(icicle); - + } + + private void postPerformCreate(Activity activity) { if (mActivityMonitors != null) { synchronized (mSync) { final int N = mActivityMonitors.size(); @@ -1096,6 +1089,33 @@ public class Instrumentation { } } } + + /** + * Perform calling of an activity's {@link Activity#onCreate} + * method. The default implementation simply calls through to that method. + * + * @param activity The activity being created. + * @param icicle The previously frozen state (or null) to pass through to onCreate(). + */ + public void callActivityOnCreate(Activity activity, Bundle icicle) { + prePerformCreate(activity); + activity.performCreate(icicle); + postPerformCreate(activity); + } + + /** + * Perform calling of an activity's {@link Activity#onCreate} + * method. The default implementation simply calls through to that method. + * @param activity The activity being created. + * @param icicle The previously frozen state (or null) to pass through to + * @param persistentState The previously persisted state (or null) + */ + public void callActivityOnCreate(Activity activity, Bundle icicle, + PersistableBundle persistentState) { + prePerformCreate(activity); + activity.performCreate(icicle, persistentState); + postPerformCreate(activity); + } public void callActivityOnDestroy(Activity activity) { // TODO: the following block causes intermittent hangs when using startActivity @@ -1130,7 +1150,7 @@ public class Instrumentation { /** * Perform calling of an activity's {@link Activity#onRestoreInstanceState} * method. The default implementation simply calls through to that method. - * + * * @param activity The activity being restored. * @param savedInstanceState The previously saved state being restored. */ @@ -1139,9 +1159,22 @@ public class Instrumentation { } /** + * Perform calling of an activity's {@link Activity#onRestoreInstanceState} + * method. The default implementation simply calls through to that method. + * + * @param activity The activity being restored. + * @param savedInstanceState The previously saved state being restored. + * @param persistentState The previously persisted state (or null) + */ + public void callActivityOnRestoreInstanceState(Activity activity, Bundle savedInstanceState, + PersistableBundle persistentState) { + activity.performRestoreInstanceState(savedInstanceState, persistentState); + } + + /** * Perform calling of an activity's {@link Activity#onPostCreate} method. * The default implementation simply calls through to that method. - * + * * @param activity The activity being created. * @param icicle The previously frozen state (or null) to pass through to * onPostCreate(). @@ -1151,6 +1184,19 @@ public class Instrumentation { } /** + * Perform calling of an activity's {@link Activity#onPostCreate} method. + * The default implementation simply calls through to that method. + * + * @param activity The activity being created. + * @param icicle The previously frozen state (or null) to pass through to + * onPostCreate(). + */ + public void callActivityOnPostCreate(Activity activity, Bundle icicle, + PersistableBundle persistentState) { + activity.onPostCreate(icicle, persistentState); + } + + /** * Perform calling of an activity's {@link Activity#onNewIntent} * method. The default implementation simply calls through to that method. * @@ -1215,7 +1261,7 @@ public class Instrumentation { /** * Perform calling of an activity's {@link Activity#onSaveInstanceState} * method. The default implementation simply calls through to that method. - * + * * @param activity The activity being saved. * @param outState The bundle to pass to the call. */ @@ -1224,6 +1270,18 @@ public class Instrumentation { } /** + * Perform calling of an activity's {@link Activity#onSaveInstanceState} + * method. The default implementation simply calls through to that method. + * @param activity The activity being saved. + * @param outState The bundle to pass to the call. + * @param outPersistentState The persistent bundle to pass to the call. + */ + public void callActivityOnSaveInstanceState(Activity activity, Bundle outState, + PersistableBundle outPersistentState) { + activity.performSaveInstanceState(outState, outPersistentState); + } + + /** * Perform calling of an activity's {@link Activity#onPause} method. The * default implementation simply calls through to that method. * @@ -1428,7 +1486,7 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity}, + * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)}, * 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 +1500,7 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity}, + * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)}, * 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,7 +1603,8 @@ public class Instrumentation { } /** - * Like {@link #execStartActivity}, but for starting as a particular user. + * Like {@link #execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle)}, + * 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 diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index aab6ed8..db91742a 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -44,8 +44,8 @@ public class KeyguardManager { * you to disable / reenable the keyguard. */ public class KeyguardLock { - private IBinder mToken = new Binder(); - private String mTag; + private final IBinder mToken = new Binder(); + private final String mTag; KeyguardLock(String tag) { mTag = tag; diff --git a/core/java/android/app/LauncherActivity.java b/core/java/android/app/LauncherActivity.java index 96c7246..9ec7f41 100644 --- a/core/java/android/app/LauncherActivity.java +++ b/core/java/android/app/LauncherActivity.java @@ -340,9 +340,11 @@ public abstract class LauncherActivity extends ListActivity { super.onCreate(icicle); mPackageManager = getPackageManager(); - - requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); - setProgressBarIndeterminateVisibility(true); + + if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + setProgressBarIndeterminateVisibility(true); + } onSetContentView(); mIconResizer = new IconResizer(); @@ -357,7 +359,9 @@ public abstract class LauncherActivity extends ListActivity { updateAlertTitle(); updateButtonText(); - setProgressBarIndeterminateVisibility(false); + if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH)) { + setProgressBarIndeterminateVisibility(false); + } } private void updateAlertTitle() { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index bba6caf..8dba1dc 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -16,9 +16,6 @@ package android.app; -import com.android.internal.R; -import com.android.internal.util.NotificationColorUtil; - import android.annotation.IntDef; import android.content.Context; import android.content.Intent; @@ -26,6 +23,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.PorterDuff; import android.media.AudioManager; +import android.media.session.MediaSessionToken; import android.net.Uri; import android.os.BadParcelableException; import android.os.Build; @@ -37,14 +35,21 @@ import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.widget.ProgressBar; import android.widget.RemoteViews; +import com.android.internal.R; +import com.android.internal.util.NotificationColorUtil; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.text.NumberFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; /** * A class that represents how a persistent notification is to be presented to @@ -134,7 +139,7 @@ public class Notification implements Parcelable * leave it at its default value of 0. * * @see android.widget.ImageView#setImageLevel - * @see android.graphics.drawable#setLevel + * @see android.graphics.drawable.Drawable#setLevel */ public int iconLevel; @@ -370,6 +375,14 @@ public class Notification implements Parcelable */ public static final int FLAG_LOCAL_ONLY = 0x00000100; + /** + * Bit to be bitswise-ored into the {@link #flags} field that should be + * set if this notification is the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link Builder#setGroup}. + */ + public static final int FLAG_GROUP_SUMMARY = 0x00000200; + public int flags; /** @hide */ @@ -539,6 +552,34 @@ public class Notification implements Parcelable */ public String category; + private String mGroupKey; + + /** + * Get the key used to group this notification into a cluster or stack + * with other notifications on devices which support such rendering. + */ + public String getGroup() { + return mGroupKey; + } + + private String mSortKey; + + /** + * Get a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + * <p>This sort key can also be used to order members of a notification group. See + * {@link Builder#setGroup}. + * + * @see String#compareTo(String) + */ + public String getSortKey() { + return mSortKey; + } + /** * Additional semantic data to be carried around with this Notification. * <p> @@ -649,6 +690,11 @@ 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"; + + /** + * {@link #extras} key: A string representing the name of the specific + * {@link android.app.Notification.Style} used to create this notification. + */ public static final String EXTRA_TEMPLATE = "android.template"; /** @@ -659,8 +705,8 @@ public class Notification implements Parcelable /** * @hide - * Extra added by NotificationManagerService to indicate whether a NotificationScorer - * modified the Notifications's score. + * Extra added by NotificationManagerService to indicate whether + * the Notifications's score has been modified. */ public static final String EXTRA_SCORE_MODIFIED = "android.scoreModified"; @@ -678,6 +724,24 @@ public class Notification implements Parcelable public static final String EXTRA_ALLOW_DURING_SETUP = "android.allowDuringSetup"; /** + * {@link #extras} key: A + * {@link android.content.ContentUris content URI} pointing to an image that can be displayed + * in the background when the notification is selected. The URI must point to an image stream + * suitable for passing into + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider + * URI used for this purpose must require no permissions to read the image data. + */ + public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; + + /** + * {@link #extras} key: A + * {@link android.media.session.MediaSessionToken} associated with a + * {@link android.app.Notification.MediaStyle} notification. + */ + public static final String EXTRA_MEDIA_SESSION = "android.mediaSession"; + + /** * Value for {@link #EXTRA_AS_HEADS_UP}. * @hide */ @@ -700,48 +764,180 @@ public class Notification implements Parcelable * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is * selected by the user. * <p> - * Apps should use {@link Builder#addAction(int, CharSequence, PendingIntent)} to create and - * attach actions. + * Apps should use {@link Notification.Builder#addAction(int, CharSequence, PendingIntent)} + * or {@link Notification.Builder#addAction(Notification.Action)} + * to attach actions. */ public static class Action implements Parcelable { + private final Bundle mExtras; + private final RemoteInput[] mRemoteInputs; + /** * Small icon representing the action. */ public int icon; + /** * Title of the action. */ public CharSequence title; + /** * Intent to send when the user invokes this action. May be null, in which case the action * may be rendered in a disabled presentation by the system UI. */ public PendingIntent actionIntent; - - private Action() { } + private Action(Parcel in) { icon = in.readInt(); title = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); if (in.readInt() == 1) { actionIntent = PendingIntent.CREATOR.createFromParcel(in); } + mExtras = in.readBundle(); + mRemoteInputs = in.createTypedArray(RemoteInput.CREATOR); } + /** - * Use {@link Builder#addAction(int, CharSequence, PendingIntent)}. + * Use {@link Notification.Builder#addAction(int, CharSequence, PendingIntent)}. */ public Action(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle(), null); + } + + private Action(int icon, CharSequence title, PendingIntent intent, Bundle extras, + RemoteInput[] remoteInputs) { this.icon = icon; this.title = title; this.actionIntent = intent; + this.mExtras = extras != null ? extras : new Bundle(); + this.mRemoteInputs = remoteInputs; + } + + /** + * Get additional metadata carried around with this Action. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Get the list of inputs to be collected from the user when this action is sent. + * May return null if no remote inputs were added. + */ + public RemoteInput[] getRemoteInputs() { + return mRemoteInputs; + } + + /** + * Builder class for {@link Action} objects. + */ + public static final class Builder { + private final int mIcon; + private final CharSequence mTitle; + private final PendingIntent mIntent; + private final Bundle mExtras; + private ArrayList<RemoteInput> mRemoteInputs; + + /** + * Construct a new builder for {@link Action} object. + * @param icon icon to show for this action + * @param title the title of the action + * @param intent the {@link PendingIntent} to fire when users trigger this action + */ + public Builder(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle(), null); + } + + /** + * Construct a new builder for {@link Action} object using the fields from an + * {@link Action}. + * @param action the action to read fields from. + */ + public Builder(Action action) { + this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras), + action.getRemoteInputs()); + } + + private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras, + RemoteInput[] remoteInputs) { + mIcon = icon; + mTitle = title; + mIntent = intent; + mExtras = extras; + if (remoteInputs != null) { + mRemoteInputs = new ArrayList<RemoteInput>(remoteInputs.length); + Collections.addAll(mRemoteInputs, remoteInputs); + } + } + + /** + * Merge additional metadata into this builder. + * + * <p>Values within the Bundle will replace existing extras values in this Builder. + * + * @see Notification.Action#extras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + * <p>The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Add an input to be collected from the user when this action is sent. + * Response values can be retrieved from the fired intent by using the + * {@link RemoteInput#getResultsFromIntent} function. + * @param remoteInput a {@link RemoteInput} to add to the action + * @return this object for method chaining + */ + public Builder addRemoteInput(RemoteInput remoteInput) { + if (mRemoteInputs == null) { + mRemoteInputs = new ArrayList<RemoteInput>(); + } + mRemoteInputs.add(remoteInput); + return this; + } + + /** + * Apply an extender to this action builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link Action} + * object. + * @return the built action + */ + public Action build() { + RemoteInput[] remoteInputs = mRemoteInputs != null + ? mRemoteInputs.toArray(new RemoteInput[mRemoteInputs.size()]) : null; + return new Action(mIcon, mTitle, mIntent, mExtras, remoteInputs); + } } @Override public Action clone() { return new Action( - this.icon, - this.title, - this.actionIntent // safe to alias - ); + icon, + title, + actionIntent, // safe to alias + new Bundle(mExtras), + getRemoteInputs()); } @Override public int describeContents() { @@ -757,9 +953,11 @@ public class Notification implements Parcelable } else { out.writeInt(0); } + out.writeBundle(mExtras); + out.writeTypedArray(mRemoteInputs, flags); } - public static final Parcelable.Creator<Action> CREATOR - = new Parcelable.Creator<Action>() { + public static final Parcelable.Creator<Action> CREATOR = + new Parcelable.Creator<Action>() { public Action createFromParcel(Parcel in) { return new Action(in); } @@ -767,6 +965,120 @@ public class Notification implements Parcelable return new Action[size]; } }; + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on an action builder. + */ + public interface Extender { + /** + * Apply this extender to a notification action builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Wearable extender for notification actions. To add extensions to an action, + * create a new {@link android.app.Notification.Action.WearableExtender} object using + * the {@code WearableExtender()} constructor and apply it to a + * {@link android.app.Notification.Action.Builder} using + * {@link android.app.Notification.Action.Builder#extend}. + * + * <pre class="prettyprint"> + * Notification.Action action = new Notification.Action.Builder( + * R.drawable.archive_all, "Archive all", actionIntent) + * .extend(new Notification.Action.WearableExtender() + * .setAvailableOffline(false)) + * .build();</pre> + */ + public static final class WearableExtender implements Extender { + /** Notification action extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + private static final String KEY_FLAGS = "flags"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_AVAILABLE_OFFLINE = 0x1; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE; + + private int mFlags = DEFAULT_FLAGS; + + /** + * Create a {@link android.app.Notification.Action.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + /** + * Create a {@link android.app.Notification.Action.WearableExtender} by reading + * wearable options present in an existing notification action. + * @param action the notification action to inspect. + */ + public WearableExtender(Action action) { + Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS); + if (wearableBundle != null) { + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + } + } + + /** + * Apply wearable extensions to a notification action that is being built. This is + * typically called by the {@link android.app.Notification.Action.Builder#extend} + * method of {@link android.app.Notification.Action.Builder}. + */ + @Override + public Action.Builder extend(Action.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mFlags = this.mFlags; + return that; + } + + /** + * Set whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device is + * offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public WearableExtender setAvailableOffline(boolean availableOffline) { + setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline); + return this; + } + + /** + * Get whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device is + * offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public boolean isAvailableOffline() { + return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + } } /** @@ -876,6 +1188,10 @@ public class Notification implements Parcelable category = parcel.readString(); + mGroupKey = parcel.readString(); + + mSortKey = parcel.readString(); + extras = parcel.readBundle(); // may be null actions = parcel.createTypedArray(Action.CREATOR); // may be null @@ -953,6 +1269,10 @@ public class Notification implements Parcelable that.category = this.category; + that.mGroupKey = this.mGroupKey; + + that.mSortKey = this.mSortKey; + if (this.extras != null) { try { that.extras = new Bundle(this.extras); @@ -1104,6 +1424,10 @@ public class Notification implements Parcelable parcel.writeString(category); + parcel.writeString(mGroupKey); + + parcel.writeString(mSortKey); + parcel.writeBundle(extras); // null ok parcel.writeTypedArray(actions, 0); // null ok @@ -1241,7 +1565,18 @@ public class Notification implements Parcelable sb.append(" flags=0x"); sb.append(Integer.toHexString(this.flags)); sb.append(String.format(" color=0x%08x", this.color)); - sb.append(" category="); sb.append(this.category); + if (this.category != null) { + sb.append(" category="); + sb.append(this.category); + } + if (this.mGroupKey != null) { + sb.append(" groupKey="); + sb.append(this.mGroupKey); + } + if (this.mSortKey != null) { + sb.append(" sortKey="); + sb.append(this.mSortKey); + } if (actions != null) { sb.append(" "); sb.append(actions.length); @@ -1324,6 +1659,8 @@ public class Notification implements Parcelable private int mProgress; private boolean mProgressIndeterminate; private String mCategory; + private String mGroupKey; + private String mSortKey; private Bundle mExtras; private int mPriority; private ArrayList<Action> mActions = new ArrayList<Action>(MAX_ACTION_BUTTONS); @@ -1334,7 +1671,6 @@ public class Notification implements Parcelable private Notification mPublicVersion = null; private final NotificationColorUtil mColorUtil; private ArrayList<String> mPeople; - private boolean mPreQuantum; private int mColor = COLOR_DEFAULT; /** @@ -1357,6 +1693,15 @@ public class Notification implements Parcelable * object. */ public Builder(Context context) { + /* + * Important compatibility note! + * Some apps out in the wild create a Notification.Builder in their Activity subclass + * constructor for later use. At this point Activities - themselves subclasses of + * ContextWrapper - do not have their inner Context populated yet. This means that + * any calls to Context methods from within this constructor can cause NPEs in existing + * apps. Any data populated from mContext should therefore be populated lazily to + * preserve compatibility. + */ mContext = context; // Set defaults to match the defaults of a Notification @@ -1365,14 +1710,13 @@ public class Notification implements Parcelable mPriority = PRIORITY_DEFAULT; mPeople = new ArrayList<String>(); - mPreQuantum = context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.L; mColorUtil = NotificationColorUtil.getInstance(); } /** * Add a timestamp pertaining to the notification (usually the time the event occurred). * It will be shown in the notification content view by default; use - * {@link Builder#setShowWhen(boolean) setShowWhen} to control this. + * {@link #setShowWhen(boolean) setShowWhen} to control this. * * @see Notification#when */ @@ -1382,7 +1726,7 @@ public class Notification implements Parcelable } /** - * Control whether the timestamp set with {@link Builder#setWhen(long) setWhen} is shown + * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown * in the content view. */ public Builder setShowWhen(boolean show) { @@ -1755,17 +2099,64 @@ public class Notification implements Parcelable } /** + * Set this notification to be part of a group of notifications sharing the same key. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. + * + * <p>To make this notification the summary for its group, also call + * {@link #setGroupSummary}. A sort order can be specified for group members by using + * {@link #setSortKey}. + * @param groupKey The group key of the group. + * @return this object for method chaining + */ + public Builder setGroup(String groupKey) { + mGroupKey = groupKey; + return this; + } + + /** + * Set this notification to be the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link #setGroup}. + * @param isGroupSummary Whether this notification should be a group summary. + * @return this object for method chaining + */ + public Builder setGroupSummary(boolean isGroupSummary) { + setFlag(FLAG_GROUP_SUMMARY, isGroupSummary); + return this; + } + + /** + * Set a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + * <p>This sort key can also be used to order members of a notification group. See + * {@link #setGroup}. + * + * @see String#compareTo(String) + */ + public Builder setSortKey(String sortKey) { + mSortKey = sortKey; + return this; + } + + /** * Merge additional metadata into this notification. * * <p>Values within the Bundle will replace existing extras values in this Builder. * * @see Notification#extras */ - public Builder addExtras(Bundle bag) { - if (mExtras == null) { - mExtras = new Bundle(bag); - } else { - mExtras.putAll(bag); + public Builder addExtras(Bundle extras) { + if (extras != null) { + if (mExtras == null) { + mExtras = new Bundle(extras); + } else { + mExtras.putAll(extras); + } } return this; } @@ -1782,8 +2173,8 @@ public class Notification implements Parcelable * * @see Notification#extras */ - public Builder setExtras(Bundle bag) { - mExtras = bag; + public Builder setExtras(Bundle extras) { + mExtras = extras; return this; } @@ -1827,6 +2218,26 @@ public class Notification implements Parcelable } /** + * Add an action to this notification. Actions are typically displayed by + * the system as a button adjacent to the notification content. + * <p> + * Every action must have an icon (32dp square and matching the + * <a href="{@docRoot}design/style/iconography.html#action-bar">Holo + * Dark action bar</a> visual style), a textual label, and a {@link PendingIntent}. + * <p> + * A notification in its expanded form can display up to 3 actions, from left to right in + * the order they were added. Actions will not be displayed when the notification is + * collapsed, however, so be sure that any essential functions may be accessed by the user + * in some other way (for example, in the Activity pointed to by {@link #contentIntent}). + * + * @param action The action to add. + */ + public Builder addAction(Action action) { + mActions.add(action); + return this; + } + + /** * Add a rich notification style to be applied at build time. * * @param style Object responsible for modifying the notification style. @@ -1843,7 +2254,7 @@ public class Notification implements Parcelable /** * Specify the value of {@link #visibility}. - + * * @param visibility One of {@link #VISIBILITY_PRIVATE} (the default), * {@link #VISIBILITY_SECRET}, or {@link #VISIBILITY_PUBLIC}. * @@ -1865,6 +2276,15 @@ public class Notification implements Parcelable return this; } + /** + * Apply an extender to this notification builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + private void setFlag(int mask, boolean value) { if (value) { mFlags |= mask; @@ -1889,26 +2309,20 @@ public class Notification implements Parcelable RemoteViews contentView = new RemoteViews(mContext.getPackageName(), resId); boolean showLine3 = false; boolean showLine2 = false; - int smallIconImageViewId = R.id.icon; + if (mPriority < PRIORITY_LOW) { // TODO: Low priority presentation } if (mLargeIcon != null) { contentView.setImageViewBitmap(R.id.icon, mLargeIcon); processLargeIcon(mLargeIcon, contentView); - smallIconImageViewId = R.id.right_icon; - } - if (mSmallIcon != 0) { - contentView.setImageViewResource(smallIconImageViewId, mSmallIcon); - contentView.setViewVisibility(smallIconImageViewId, View.VISIBLE); - if (mLargeIcon != null) { - processSmallRightIcon(mSmallIcon, smallIconImageViewId, contentView); - } else { - processSmallIconAsLarge(mSmallIcon, contentView); - } - - } else { - contentView.setViewVisibility(smallIconImageViewId, View.GONE); + contentView.setImageViewResource(R.id.right_icon, mSmallIcon); + contentView.setViewVisibility(R.id.right_icon, View.VISIBLE); + processSmallRightIcon(mSmallIcon, contentView); + } else { // small icon at left + contentView.setImageViewResource(R.id.icon, mSmallIcon); + contentView.setViewVisibility(R.id.icon, View.VISIBLE); + processSmallIconAsLarge(mSmallIcon, contentView); } if (mContentTitle != null) { contentView.setTextViewText(R.id.title, processLegacyText(mContentTitle)); @@ -1995,14 +2409,12 @@ public class Notification implements Parcelable int N = mActions.size(); if (N > 0) { - // Log.d("Notification", "has actions: " + mContentText); big.setViewVisibility(R.id.actions, View.VISIBLE); big.setViewVisibility(R.id.action_divider, View.VISIBLE); if (N>MAX_ACTION_BUTTONS) N=MAX_ACTION_BUTTONS; big.removeAllViews(R.id.actions); for (int i=0; i<N; i++) { final RemoteViews button = generateActionButton(mActions.get(i)); - //Log.d("Notification", "adding action " + i + ": " + mActions.get(i).title); big.addView(R.id.actions, button); } } @@ -2103,6 +2515,8 @@ public class Notification implements Parcelable private void processLargeIcon(Bitmap largeIcon, RemoteViews contentView) { if (!isLegacy() || mColorUtil.isGrayscale(largeIcon)) { applyLargeIconBackground(contentView); + } else { + removeLargeIconBackground(contentView); } } @@ -2122,16 +2536,31 @@ public class Notification implements Parcelable -1); } + private void removeLargeIconBackground(RemoteViews contentView) { + contentView.setInt(R.id.icon, "setBackgroundResource", 0); + } + /** * Recolor small icons when used in the R.id.right_icon slot. */ - private void processSmallRightIcon(int smallIconDrawableId, int smallIconImageViewId, + private void processSmallRightIcon(int smallIconDrawableId, RemoteViews contentView) { if (!isLegacy() || mColorUtil.isGrayscale(mContext, smallIconDrawableId)) { - contentView.setDrawableParameters(smallIconImageViewId, false, -1, - mContext.getResources().getColor( - R.color.notification_action_legacy_color_filter), - PorterDuff.Mode.MULTIPLY, -1); + contentView.setDrawableParameters(R.id.right_icon, false, -1, + 0xFFFFFFFF, + PorterDuff.Mode.SRC_ATOP, -1); + + contentView.setInt(R.id.right_icon, + "setBackgroundResource", + R.drawable.notification_icon_legacy_bg); + + contentView.setDrawableParameters( + R.id.right_icon, + true, + -1, + mColor, + PorterDuff.Mode.SRC_ATOP, + -1); } } @@ -2181,6 +2610,8 @@ public class Notification implements Parcelable n.flags |= FLAG_SHOW_LIGHTS; } n.category = mCategory; + n.mGroupKey = mGroupKey; + n.mSortKey = mSortKey; n.priority = mPriority; if (mActions.size() > 0) { n.actions = new Action[mActions.size()]; @@ -2692,4 +3123,808 @@ public class Notification implements Parcelable return wip; } } + + /** + * Notification style for media playback notifications. + * + * In the expanded form, {@link Notification#bigContentView}, up to 5 + * {@link Notification.Action}s specified with + * {@link Notification.Builder#addAction(int, CharSequence, PendingIntent) addAction} will be + * shown as icon-only pushbuttons, suitable for transport controls. The Bitmap given to + * {@link Notification.Builder#setLargeIcon(android.graphics.Bitmap) setLargeIcon()} will be + * treated as album artwork. + * + * Unlike the other styles provided here, MediaStyle can also modify the standard-size + * {@link Notification#contentView}; by providing action indices to + * {@link #setShowActionsInCompactView(int...)} you can promote up to 2 actions to be displayed + * in the standard view alongside the usual content. + * + * Finally, if you attach a {@link android.media.session.MediaSessionToken} using + * {@link android.app.Notification.MediaStyle#setMediaSession(MediaSessionToken)}, + * the System UI can identify this as a notification representing an active media session + * and respond accordingly (by showing album artwork in the lockscreen, for example). + * + * To use this style with your Notification, feed it to + * {@link Notification.Builder#setStyle(android.app.Notification.Style)} like so: + * <pre class="prettyprint"> + * Notification noti = new Notification.Builder() + * .setSmallIcon(R.drawable.ic_stat_player) + * .setContentTitle("Track title") // these three lines are optional + * .setContentText("Artist - Album") // if you use + * .setLargeIcon(albumArtBitmap)) // setMediaSession(token, true) + * .setMediaSession(mySession, true) + * .setStyle(<b>new Notification.MediaStyle()</b>) + * .build(); + * </pre> + * + * @see Notification#bigContentView + */ + public static class MediaStyle extends Style { + static final int MAX_MEDIA_BUTTONS_IN_COMPACT = 2; + static final int MAX_MEDIA_BUTTONS = 5; + + private int[] mActionsToShowInCompact = null; + private MediaSessionToken mToken; + + public MediaStyle() { + } + + public MediaStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Request up to 2 actions (by index in the order of addition) to be shown in the compact + * notification view. + */ + public MediaStyle setShowActionsInCompactView(int...actions) { + mActionsToShowInCompact = actions; + return this; + } + + /** + * Attach a {@link android.media.session.MediaSessionToken} to this Notification to provide + * additional playback information and control to the SystemUI. + */ + public MediaStyle setMediaSession(MediaSessionToken token) { + mToken = token; + return this; + } + + @Override + public Notification buildStyled(Notification wip) { + wip.contentView = makeMediaContentView(); + wip.bigContentView = makeMediaBigContentView(); + + return wip; + } + + /** @hide */ + @Override + public void addExtras(Bundle extras) { + super.addExtras(extras); + + if (mToken != null) { + extras.putParcelable(EXTRA_MEDIA_SESSION, mToken); + } + } + + private RemoteViews generateMediaActionButton(Action action) { + final boolean tombstone = (action.actionIntent == null); + RemoteViews button = new RemoteViews(mBuilder.mContext.getPackageName(), + R.layout.notification_quantum_media_action); + button.setImageViewResource(R.id.action0, action.icon); + if (!tombstone) { + button.setOnClickPendingIntent(R.id.action0, action.actionIntent); + } + button.setContentDescription(R.id.action0, action.title); + return button; + } + + private RemoteViews makeMediaContentView() { + RemoteViews view = mBuilder.applyStandardTemplate( + R.layout.notification_template_quantum_media, true /* 1U */); + + final int numActions = mBuilder.mActions.size(); + final int N = mActionsToShowInCompact == null + ? 0 + : Math.min(mActionsToShowInCompact.length, MAX_MEDIA_BUTTONS_IN_COMPACT); + if (N > 0) { + view.removeAllViews(R.id.actions); + for (int i = 0; i < N; i++) { + if (i >= numActions) { + throw new IllegalArgumentException(String.format( + "setShowActionsInCompactView: action %d out of bounds (max %d)", + i, numActions - 1)); + } + + final Action action = mBuilder.mActions.get(mActionsToShowInCompact[i]); + final RemoteViews button = generateMediaActionButton(action); + view.addView(R.id.actions, button); + } + } + return view; + } + + private RemoteViews makeMediaBigContentView() { + RemoteViews big = mBuilder.applyStandardTemplate( + R.layout.notification_template_quantum_big_media, false); + + final int N = Math.min(mBuilder.mActions.size(), MAX_MEDIA_BUTTONS); + if (N > 0) { + big.removeAllViews(R.id.actions); + for (int i=0; i<N; i++) { + final RemoteViews button = generateMediaActionButton(mBuilder.mActions.get(i)); + big.addView(R.id.actions, button); + } + } + return big; + } + } + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on a notification builder. + */ + public interface Extender { + /** + * Apply this extender to a notification builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Helper class to add wearable extensions to notifications. + * <p class="note"> See + * <a href="{@docRoot}wear/notifications/creating.html">Creating Notifications + * for Android Wear</a> for more information on how to use this class. + * <p> + * To create a notification with wearable extensions: + * <ol> + * <li>Create a {@link android.app.Notification.Builder}, setting any desired + * properties. + * <li>Create a {@link android.app.Notification.WearableExtender}. + * <li>Set wearable-specific properties using the + * {@code add} and {@code set} methods of {@link android.app.Notification.WearableExtender}. + * <li>Call {@link android.app.Notification.Builder#extend} to apply the extensions to a + * notification. + * <li>Post the notification to the notification system with the + * {@code NotificationManager.notify(...)} methods. + * </ol> + * + * <pre class="prettyprint"> + * Notification notif = new Notification.Builder(mContext) + * .setContentTitle("New mail from " + sender.toString()) + * .setContentText(subject) + * .setSmallIcon(R.drawable.new_mail) + * .extend(new Notification.WearableExtender() + * .setContentIcon(R.drawable.new_mail)) + * .build(); + * NotificationManager notificationManger = + * (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + * notificationManger.notify(0, notif);</pre> + * + * <p>Wearable extensions can be accessed on an existing notification by using the + * {@code WearableExtender(Notification)} constructor, + * and then using the {@code get} methods to access values. + * + * <pre class="prettyprint"> + * Notification.WearableExtender wearableExtender = new Notification.WearableExtender( + * notification); + * List<Notification> pages = wearableExtender.getPages();</pre> + */ + public static final class WearableExtender implements Extender { + /** + * Sentinel value for an action index that is unset. + */ + public static final int UNSET_ACTION_INDEX = -1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification with + * default sizing. + * <p>For custom display notifications created using {@link #setDisplayIntent}, + * the default is {@link #SIZE_LARGE}. All other notifications size automatically based + * on their content. + */ + public static final int SIZE_DEFAULT = 0; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with an extra small size. + * <p>This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_XSMALL = 1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a small size. + * <p>This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_SMALL = 2; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a medium size. + * <p>This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_MEDIUM = 3; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a large size. + * <p>This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_LARGE = 4; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * full screen. + * <p>This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_FULL_SCREEN = 5; + + /** Notification extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + // Keys within EXTRA_WEARABLE_OPTIONS for wearable options. + private static final String KEY_ACTIONS = "actions"; + private static final String KEY_FLAGS = "flags"; + private static final String KEY_DISPLAY_INTENT = "displayIntent"; + private static final String KEY_PAGES = "pages"; + private static final String KEY_BACKGROUND = "background"; + private static final String KEY_CONTENT_ICON = "contentIcon"; + private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity"; + private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex"; + private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset"; + private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight"; + private static final String KEY_GRAVITY = "gravity"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1; + private static final int FLAG_HINT_HIDE_ICON = 1 << 1; + private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2; + private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE; + + private static final int DEFAULT_CONTENT_ICON_GRAVITY = Gravity.END; + private static final int DEFAULT_GRAVITY = Gravity.BOTTOM; + + private ArrayList<Action> mActions = new ArrayList<Action>(); + private int mFlags = DEFAULT_FLAGS; + private PendingIntent mDisplayIntent; + private ArrayList<Notification> mPages = new ArrayList<Notification>(); + private Bitmap mBackground; + private int mContentIcon; + private int mContentIconGravity = DEFAULT_CONTENT_ICON_GRAVITY; + private int mContentActionIndex = UNSET_ACTION_INDEX; + private int mCustomSizePreset = SIZE_DEFAULT; + private int mCustomContentHeight; + private int mGravity = DEFAULT_GRAVITY; + + /** + * Create a {@link android.app.Notification.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + public WearableExtender(Notification notif) { + Bundle wearableBundle = notif.extras.getBundle(EXTRA_WEARABLE_EXTENSIONS); + if (wearableBundle != null) { + List<Action> actions = wearableBundle.getParcelableArrayList(KEY_ACTIONS); + if (actions != null) { + mActions.addAll(actions); + } + + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mDisplayIntent = wearableBundle.getParcelable(KEY_DISPLAY_INTENT); + + Notification[] pages = getNotificationArrayFromBundle( + wearableBundle, KEY_PAGES); + if (pages != null) { + Collections.addAll(mPages, pages); + } + + mBackground = wearableBundle.getParcelable(KEY_BACKGROUND); + mContentIcon = wearableBundle.getInt(KEY_CONTENT_ICON); + mContentIconGravity = wearableBundle.getInt(KEY_CONTENT_ICON_GRAVITY, + DEFAULT_CONTENT_ICON_GRAVITY); + mContentActionIndex = wearableBundle.getInt(KEY_CONTENT_ACTION_INDEX, + UNSET_ACTION_INDEX); + mCustomSizePreset = wearableBundle.getInt(KEY_CUSTOM_SIZE_PRESET, + SIZE_DEFAULT); + mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT); + mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY); + } + } + + /** + * Apply wearable extensions to a notification that is being built. This is typically + * called by the {@link android.app.Notification.Builder#extend} method of + * {@link android.app.Notification.Builder}. + */ + @Override + public Notification.Builder extend(Notification.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (!mActions.isEmpty()) { + wearableBundle.putParcelableArrayList(KEY_ACTIONS, mActions); + } + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + if (mDisplayIntent != null) { + wearableBundle.putParcelable(KEY_DISPLAY_INTENT, mDisplayIntent); + } + if (!mPages.isEmpty()) { + wearableBundle.putParcelableArray(KEY_PAGES, mPages.toArray( + new Notification[mPages.size()])); + } + if (mBackground != null) { + wearableBundle.putParcelable(KEY_BACKGROUND, mBackground); + } + if (mContentIcon != 0) { + wearableBundle.putInt(KEY_CONTENT_ICON, mContentIcon); + } + if (mContentIconGravity != DEFAULT_CONTENT_ICON_GRAVITY) { + wearableBundle.putInt(KEY_CONTENT_ICON_GRAVITY, mContentIconGravity); + } + if (mContentActionIndex != UNSET_ACTION_INDEX) { + wearableBundle.putInt(KEY_CONTENT_ACTION_INDEX, + mContentActionIndex); + } + if (mCustomSizePreset != SIZE_DEFAULT) { + wearableBundle.putInt(KEY_CUSTOM_SIZE_PRESET, mCustomSizePreset); + } + if (mCustomContentHeight != 0) { + wearableBundle.putInt(KEY_CUSTOM_CONTENT_HEIGHT, mCustomContentHeight); + } + if (mGravity != DEFAULT_GRAVITY) { + wearableBundle.putInt(KEY_GRAVITY, mGravity); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mActions = new ArrayList<Action>(this.mActions); + that.mFlags = this.mFlags; + that.mDisplayIntent = this.mDisplayIntent; + that.mPages = new ArrayList<Notification>(this.mPages); + that.mBackground = this.mBackground; + that.mContentIcon = this.mContentIcon; + that.mContentIconGravity = this.mContentIconGravity; + that.mContentActionIndex = this.mContentActionIndex; + that.mCustomSizePreset = this.mCustomSizePreset; + that.mCustomContentHeight = this.mCustomContentHeight; + that.mGravity = this.mGravity; + return that; + } + + /** + * Add a wearable action to this notification. + * + * <p>When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link android.app.Notification.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param action the action to add to this notification + * @return this object for method chaining + * @see android.app.Notification.Action + */ + public WearableExtender addAction(Action action) { + mActions.add(action); + return this; + } + + /** + * Adds wearable actions to this notification. + * + * <p>When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link android.app.Notification.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param actions the actions to add to this notification + * @return this object for method chaining + * @see android.app.Notification.Action + */ + public WearableExtender addActions(List<Action> actions) { + mActions.addAll(actions); + return this; + } + + /** + * Clear all wearable actions present on this builder. + * @return this object for method chaining. + * @see #addAction + */ + public WearableExtender clearActions() { + mActions.clear(); + return this; + } + + /** + * Get the wearable actions present on this notification. + */ + public List<Action> getActions() { + return mActions; + } + + /** + * Set an intent to launch inside of an activity view when displaying + * this notification. The {@link PendingIntent} provided should be for an activity. + * + * <pre class="prettyprint"> + * Intent displayIntent = new Intent(context, MyDisplayActivity.class); + * PendingIntent displayPendingIntent = PendingIntent.getActivity(context, + * 0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT); + * Notification notif = new Notification.Builder(context) + * .extend(new Notification.WearableExtender() + * .setDisplayIntent(displayPendingIntent) + * .setCustomSizePreset(Notification.WearableExtender.SIZE_MEDIUM)) + * .build();</pre> + * + * <p>The activity to launch needs to allow embedding, must be exported, and + * should have an empty task affinity. + * + * <p>Example AndroidManifest.xml entry: + * <pre class="prettyprint"> + * <activity android:name="com.example.MyDisplayActivity" + * android:exported="true" + * android:allowEmbedded="true" + * android:taskAffinity="" /></pre> + * + * @param intent the {@link PendingIntent} for an activity + * @return this object for method chaining + * @see android.app.Notification.WearableExtender#getDisplayIntent + */ + public WearableExtender setDisplayIntent(PendingIntent intent) { + mDisplayIntent = intent; + return this; + } + + /** + * Get the intent to launch inside of an activity view when displaying this + * notification. This {@code PendingIntent} should be for an activity. + */ + public PendingIntent getDisplayIntent() { + return mDisplayIntent; + } + + /** + * Add an additional page of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param page the notification to add as another page + * @return this object for method chaining + * @see android.app.Notification.WearableExtender#getPages + */ + public WearableExtender addPage(Notification page) { + mPages.add(page); + return this; + } + + /** + * Add additional pages of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param pages a list of notifications + * @return this object for method chaining + * @see android.app.Notification.WearableExtender#getPages + */ + public WearableExtender addPages(List<Notification> pages) { + mPages.addAll(pages); + return this; + } + + /** + * Clear all additional pages present on this builder. + * @return this object for method chaining. + * @see #addPage + */ + public WearableExtender clearPages() { + mPages.clear(); + return this; + } + + /** + * Get the array of additional pages of content for displaying this notification. The + * current notification forms the first page, and elements within this array form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * @return the pages for this notification + */ + public List<Notification> getPages() { + return mPages; + } + + /** + * Set a background image to be displayed behind the notification content. + * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background + * will work with any notification style. + * + * @param background the background bitmap + * @return this object for method chaining + * @see android.app.Notification.WearableExtender#getBackground + */ + public WearableExtender setBackground(Bitmap background) { + mBackground = background; + return this; + } + + /** + * Get a background image to be displayed behind the notification content. + * Contrary to the {@link android.app.Notification.BigPictureStyle}, this background + * will work with any notification style. + * + * @return the background image + * @see android.app.Notification.WearableExtender#setBackground + */ + public Bitmap getBackground() { + return mBackground; + } + + /** + * Set an icon that goes with the content of this notification. + */ + public WearableExtender setContentIcon(int icon) { + mContentIcon = icon; + return this; + } + + /** + * Get an icon that goes with the content of this notification. + */ + public int getContentIcon() { + return mContentIcon; + } + + /** + * Set the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #setContentIcon + */ + public WearableExtender setContentIconGravity(int contentIconGravity) { + mContentIconGravity = contentIconGravity; + return this; + } + + /** + * Get the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #getContentIcon + */ + public int getContentIconGravity() { + return mContentIconGravity; + } + + /** + * Set an action from this notification's actions to be clickable with the content of + * this notification. This action will no longer display separately from the + * notification's content. + * + * <p>For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + * @param actionIndex The index of the action to hoist onto the current notification page. + * If wearable actions were added to the main notification, this index + * will apply to that list, otherwise it will apply to the regular + * actions list. + */ + public WearableExtender setContentAction(int actionIndex) { + mContentActionIndex = actionIndex; + return this; + } + + /** + * Get the index of the notification action, if any, that was specified as being clickable + * with the content of this notification. This action will no longer display separately + * from the notification's content. + * + * <p>For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + * <p>If wearable specific actions were added to the main notification, this index will + * apply to that list, otherwise it will apply to the regular actions list. + * + * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected. + */ + public int getContentAction() { + return mContentActionIndex; + } + + /** + * Set the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public WearableExtender setGravity(int gravity) { + mGravity = gravity; + return this; + } + + /** + * Get the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public int getGravity() { + return mGravity; + } + + /** + * Set the custom size preset for the display of this notification out of the available + * presets found in {@link android.app.Notification.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + * <p>Some custom size presets are only applicable for custom display notifications created + * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. Check the + * documentation for the preset in question. See also + * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}. + */ + public WearableExtender setCustomSizePreset(int sizePreset) { + mCustomSizePreset = sizePreset; + return this; + } + + /** + * Get the custom size preset for the display of this notification out of the available + * presets found in {@link android.app.Notification.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + * <p>Some custom size presets are only applicable for custom display notifications created + * using {@link #setDisplayIntent}. Check the documentation for the preset in question. + * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}. + */ + public int getCustomSizePreset() { + return mCustomSizePreset; + } + + /** + * Set the custom height in pixels for the display of this notification's content. + * <p>This option is only available for custom display notifications created + * using {@link android.app.Notification.WearableExtender#setDisplayIntent}. See also + * {@link android.app.Notification.WearableExtender#setCustomSizePreset} and + * {@link #getCustomContentHeight}. + */ + public WearableExtender setCustomContentHeight(int height) { + mCustomContentHeight = height; + return this; + } + + /** + * Get the custom height in pixels for the display of this notification's content. + * <p>This option is only available for custom display notifications created + * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and + * {@link #setCustomContentHeight}. + */ + public int getCustomContentHeight() { + return mCustomContentHeight; + } + + /** + * Set whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public WearableExtender setStartScrollBottom(boolean startScrollBottom) { + setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom); + return this; + } + + /** + * Get whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public boolean getStartScrollBottom() { + return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0; + } + + /** + * Set whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public WearableExtender setContentIntentAvailableOffline( + boolean contentIntentAvailableOffline) { + setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline); + return this; + } + + /** + * Get whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public boolean getContentIntentAvailableOffline() { + return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0; + } + + /** + * Set a hint that this notification's icon should not be displayed. + * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise. + * @return this object for method chaining + */ + public WearableExtender setHintHideIcon(boolean hintHideIcon) { + setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon); + return this; + } + + /** + * Get a hint that this notification's icon should not be displayed. + * @return {@code true} if this icon should not be displayed, false otherwise. + * The default value is {@code false} if this was never set. + */ + public boolean getHintHideIcon() { + return (mFlags & FLAG_HINT_HIDE_ICON) != 0; + } + + /** + * Set a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link #addPage}. + */ + public WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) { + setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly); + return this; + } + + /** + * Get a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link android.app.Notification.WearableExtender#addPage}. + */ + public boolean getHintShowBackgroundOnly() { + return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + } + + /** + * Get an array of Notification objects from a parcelable array bundle field. + * Update the bundle to have a typed array so fetches in the future don't need + * to do an array copy. + */ + private static Notification[] getNotificationArrayFromBundle(Bundle bundle, String key) { + Parcelable[] array = bundle.getParcelableArray(key); + if (array instanceof Notification[] || array == null) { + return (Notification[]) array; + } + Notification[] typedArray = Arrays.copyOf(array, array.length, + Notification[].class); + bundle.putParcelableArray(key, typedArray); + return typedArray; + } } diff --git a/core/java/android/app/PackageInstallObserver.java b/core/java/android/app/PackageInstallObserver.java index dacffb4..941efbd 100644 --- a/core/java/android/app/PackageInstallObserver.java +++ b/core/java/android/app/PackageInstallObserver.java @@ -18,32 +18,36 @@ 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. - */ +/** {@hide} */ public class PackageInstallObserver { - IPackageInstallObserver2.Stub mObserver = new IPackageInstallObserver2.Stub() { + private final IPackageInstallObserver2.Stub mBinder = new IPackageInstallObserver2.Stub() { @Override - public void packageInstalled(String pkgName, Bundle extras, int result) - throws RemoteException { - PackageInstallObserver.this.packageInstalled(pkgName, extras, result); + public void packageInstalled(String basePackageName, Bundle extras, int returnCode) { + PackageInstallObserver.this.packageInstalled(basePackageName, extras, returnCode); } }; + /** {@hide} */ + public IPackageInstallObserver2.Stub getBinder() { + return mBinder; + } + /** - * This method will be called to report the result of the package installation attempt. + * 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 + * @param basePackageName 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 returnCode The numeric success or failure code indicating the + * basic outcome + * @hide */ - public void packageInstalled(String pkgName, Bundle extras, int result) { + public void packageInstalled(String basePackageName, Bundle extras, int returnCode) { } } diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/app/PackageUninstallObserver.java index 4f1bc2b..0a960a7 100644 --- a/core/java/android/tv/ITvInputService.aidl +++ b/core/java/android/app/PackageUninstallObserver.java @@ -14,18 +14,24 @@ * limitations under the License. */ -package android.tv; +package android.app; -import android.tv.ITvInputServiceCallback; -import android.tv.ITvInputSessionCallback; -import android.view.InputChannel; +import android.content.pm.IPackageDeleteObserver; -/** - * 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); +/** {@hide} */ +public class PackageUninstallObserver { + private final IPackageDeleteObserver.Stub mBinder = new IPackageDeleteObserver.Stub() { + @Override + public void packageDeleted(String basePackageName, int returnCode) { + PackageUninstallObserver.this.onUninstallFinished(basePackageName, returnCode); + } + }; + + /** {@hide} */ + public IPackageDeleteObserver.Stub getBinder() { + return mBinder; + } + + public void onUninstallFinished(String basePackageName, int returnCode) { + } } diff --git a/core/java/android/app/RemoteInput.java b/core/java/android/app/RemoteInput.java new file mode 100644 index 0000000..11420c5 --- /dev/null +++ b/core/java/android/app/RemoteInput.java @@ -0,0 +1,311 @@ +/* + * 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.ClipData; +import android.content.ClipDescription; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A {@code RemoteInput} object specifies input to be collected from a user to be passed along with + * an intent inside a {@link android.app.PendingIntent} that is sent. + * Always use {@link RemoteInput.Builder} to create instances of this class. + * <p class="note"> See + * <a href="{@docRoot}wear/notifications/remote-input.html">Receiving Voice Input from + * a Notification</a> for more information on how to use this class. + * + * <p>The following example adds a {@code RemoteInput} to a {@link Notification.Action}, + * sets the result key as {@code quick_reply}, and sets the label as {@code Quick reply}. + * Users are prompted to input a response when they trigger the action. The results are sent along + * with the intent and can be retrieved with the result key (provided to the {@link Builder} + * constructor) from the Bundle returned by {@link #getResultsFromIntent}. + * + * <pre class="prettyprint"> + * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply"; + * Notification.Action action = new Notification.Action.Builder( + * R.drawable.reply, "Reply", actionIntent) + * <b>.addRemoteInput(new RemoteInput.Builder(KEY_QUICK_REPLY_TEXT) + * .setLabel("Quick reply").build()</b>) + * .build();</pre> + * + * <p>When the {@link android.app.PendingIntent} is fired, the intent inside will contain the + * input results if collected. To access these results, use the {@link #getResultsFromIntent} + * function. The result values will present under the result key passed to the {@link Builder} + * constructor. + * + * <pre class="prettyprint"> + * public static final String KEY_QUICK_REPLY_TEXT = "quick_reply"; + * Bundle results = RemoteInput.getResultsFromIntent(intent); + * if (results != null) { + * CharSequence quickReplyResult = results.getCharSequence(KEY_QUICK_REPLY_TEXT); + * }</pre> + */ +public final class RemoteInput implements Parcelable { + /** Label used to denote the clip data type used for remote input transport */ + public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results"; + + /** Extra added to a clip data intent object to hold the results bundle. */ + public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_ALLOW_FREE_FORM_INPUT; + + private final String mResultKey; + private final CharSequence mLabel; + private final CharSequence[] mChoices; + private final int mFlags; + private final Bundle mExtras; + + private RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, + int flags, Bundle extras) { + this.mResultKey = resultKey; + this.mLabel = label; + this.mChoices = choices; + this.mFlags = flags; + this.mExtras = extras; + } + + /** + * Get the key that the result of this input will be set in from the Bundle returned by + * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent. + */ + public String getResultKey() { + return mResultKey; + } + + /** + * Get the label to display to users when collecting this input. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Get possible input choices. This can be {@code null} if there are no choices to present. + */ + public CharSequence[] getChoices() { + return mChoices; + } + + /** + * Get whether or not users can provide an arbitrary value for + * input. If you set this to {@code false}, users must select one of the + * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown + * if you set this to false and {@link #getChoices} returns {@code null} or empty. + */ + public boolean getAllowFreeFormInput() { + return (mFlags & FLAG_ALLOW_FREE_FORM_INPUT) != 0; + } + + /** + * Get additional metadata carried around with this remote input. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Builder class for {@link RemoteInput} objects. + */ + public static final class Builder { + private final String mResultKey; + private CharSequence mLabel; + private CharSequence[] mChoices; + private int mFlags = DEFAULT_FLAGS; + private Bundle mExtras = new Bundle(); + + /** + * Create a builder object for {@link RemoteInput} objects. + * @param resultKey the Bundle key that refers to this input when collected from the user + */ + public Builder(String resultKey) { + if (resultKey == null) { + throw new IllegalArgumentException("Result key can't be null"); + } + mResultKey = resultKey; + } + + /** + * Set a label to be displayed to the user when collecting this input. + * @param label The label to show to users when they input a response. + * @return this object for method chaining + */ + public Builder setLabel(CharSequence label) { + mLabel = Notification.safeCharSequence(label); + return this; + } + + /** + * Specifies choices available to the user to satisfy this input. + * @param choices an array of pre-defined choices for users input. + * You must provide a non-null and non-empty array if + * you disabled free form input using {@link #setAllowFreeFormInput}. + * @return this object for method chaining + */ + public Builder setChoices(CharSequence[] choices) { + if (choices == null) { + mChoices = null; + } else { + mChoices = new CharSequence[choices.length]; + for (int i = 0; i < choices.length; i++) { + mChoices[i] = Notification.safeCharSequence(choices[i]); + } + } + return this; + } + + /** + * Specifies whether the user can provide arbitrary values. + * + * @param allowFreeFormInput The default is {@code true}. + * If you specify {@code false}, you must provide a non-null + * and non-empty array to {@link #setChoices} or an + * {@link IllegalArgumentException} is thrown. + * @return this object for method chaining + */ + public Builder setAllowFreeFormInput(boolean allowFreeFormInput) { + setFlag(mFlags, allowFreeFormInput); + return this; + } + + /** + * Merge additional metadata into this builder. + * + * <p>Values within the Bundle will replace existing extras values in this Builder. + * + * @see RemoteInput#getExtras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + * <p>The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + + /** + * Combine all of the options that have been set and return a new {@link RemoteInput} + * object. + */ + public RemoteInput build() { + return new RemoteInput(mResultKey, mLabel, mChoices, mFlags, mExtras); + } + } + + private RemoteInput(Parcel in) { + mResultKey = in.readString(); + mLabel = in.readCharSequence(); + mChoices = in.readCharSequenceArray(); + mFlags = in.readInt(); + mExtras = in.readBundle(); + } + + /** + * Get the remote input results bundle from an intent. The returned Bundle will + * contain a key/value for every result key populated by remote input collector. + * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. + * @param intent The intent object that fired in response to an action or content intent + * which also had one or more remote input requested. + */ + public static Bundle getResultsFromIntent(Intent intent) { + ClipData clipData = intent.getClipData(); + if (clipData == null) { + return null; + } + ClipDescription clipDescription = clipData.getDescription(); + if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { + return null; + } + if (clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) { + return clipData.getItemAt(0).getIntent().getExtras().getParcelable(EXTRA_RESULTS_DATA); + } + return null; + } + + /** + * Populate an intent object with the results gathered from remote input. This method + * should only be called by remote input collection services when sending results to a + * pending intent. + * @param remoteInputs The remote inputs for which results are being provided + * @param intent The intent to add remote inputs to. The {@link ClipData} + * field of the intent will be modified to contain the results. + * @param results A bundle holding the remote input results. This bundle should + * be populated with keys matching the result keys specified in + * {@code remoteInputs} with values being the result per key. + */ + public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, + Bundle results) { + Bundle resultsBundle = new Bundle(); + for (RemoteInput remoteInput : remoteInputs) { + Object result = results.get(remoteInput.getResultKey()); + if (result instanceof CharSequence) { + resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result); + } + } + Intent clipIntent = new Intent(); + clipIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle); + intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipIntent)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mResultKey); + out.writeCharSequence(mLabel); + out.writeCharSequenceArray(mChoices); + out.writeInt(mFlags); + out.writeBundle(mExtras); + } + + public static final Creator<RemoteInput> CREATOR = new Creator<RemoteInput>() { + @Override + public RemoteInput createFromParcel(Parcel in) { + return new RemoteInput(in); + } + + @Override + public RemoteInput[] newArray(int size) { + return new RemoteInput[size]; + } + }; +} diff --git a/core/java/android/app/SharedElementListener.java b/core/java/android/app/SharedElementListener.java new file mode 100644 index 0000000..e03e42e --- /dev/null +++ b/core/java/android/app/SharedElementListener.java @@ -0,0 +1,114 @@ +/* + * 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.view.View; + +import java.util.List; +import java.util.Map; + +/** + * Listener provided in + * {@link Activity#setEnterSharedElementListener(SharedElementListener)} and + * {@link Activity#setExitSharedElementListener(SharedElementListener)} + * to monitor the Activity transitions. The events can be used to customize Activity + * Transition behavior. + */ +public abstract class SharedElementListener { + + static final SharedElementListener NULL_LISTENER = new SharedElementListener() { + }; + + /** + * Called to allow the listener to customize the start state of the shared element when + * transferring in shared element state. + * <p> + * The shared element will start at the size and position of the shared element + * in the launching Activity or Fragment. It will also transfer ImageView scaleType + * and imageMatrix if the shared elements in the calling and called Activities are + * ImageViews. Some applications may want to make additional changes, such as + * changing the clip bounds, scaling, or rotation if the shared element end state + * does not map well to the start state. + * </p> + * + * @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 setSharedElementStart(List<String> sharedElementNames, + List<View> sharedElements, List<View> sharedElementSnapshots) {} + + /** + * Called to allow the listener to customize the end state of the shared element when + * transferring in shared element state. + * <p> + * Any customization done in + * {@link #setSharedElementStart(java.util.List, java.util.List, java.util.List)} + * may need to be modified to the final state of the shared element if it is not + * automatically corrected by layout. For example, rotation or scale will not + * be affected by layout and if changed in {@link #setSharedElementStart(java.util.List, + * java.util.List, java.util.List)}, it will also have to be set here again to correct + * the end state. + * </p> + * + * @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 setSharedElementEnd(List<String> sharedElementNames, + List<View> sharedElements, List<View> sharedElementSnapshots) {} + + /** + * Called after {@link #remapSharedElements(java.util.List, java.util.Map)} when + * transferring shared elements in. Any shared elements that have no mapping will be in + * <var>rejectedSharedElements</var>. The elements remaining in + * <var>rejectedSharedElements</var> will be transitioned out of the Scene. If a + * View is removed from <var>rejectedSharedElements</var>, it must be handled by the + * <code>SharedElementListener</code>. + * <p> + * Views in rejectedSharedElements will have their position and size set to the + * position of the calling shared element, relative to the Window decor View and contain + * snapshots of the View from the calling Activity or Fragment. This + * view may be safely added to the decor View's overlay to remain in position. + * </p> + * + * @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. A + * View removed from this list will not be transitioned + * automatically. + */ + public void handleRejectedSharedElements(List<View> rejectedSharedElements) {} + + /** + * Lets the ActivityTransitionListener adjust the mapping of shared element names to + * Views. + * + * @param names The names of all shared elements transferred from the calling Activity + * to the started Activity. + * @param sharedElements The mapping of shared element names to Views. The best guess + * will be filled into sharedElements based on the View names. + */ + public void remapSharedElements(List<String> names, Map<String, View> sharedElements) {} +} diff --git a/core/java/android/app/TaskManagerImpl.java b/core/java/android/app/TaskManagerImpl.java new file mode 100644 index 0000000..f42839e --- /dev/null +++ b/core/java/android/app/TaskManagerImpl.java @@ -0,0 +1,62 @@ +/* + * 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. + */ + +// in android.app so ContextImpl has package access +package android.app; + +import android.app.task.ITaskManager; +import android.app.task.Task; +import android.app.task.TaskManager; + +import java.util.List; + + +/** + * Concrete implementation of the TaskManager interface + * @hide + */ +public class TaskManagerImpl extends TaskManager { + ITaskManager mBinder; + + /* package */ TaskManagerImpl(ITaskManager binder) { + mBinder = binder; + } + + @Override + public int schedule(Task task) { + // TODO Auto-generated method stub + return 0; + } + + @Override + public void cancel(int taskId) { + // TODO Auto-generated method stub + + } + + @Override + public void cancelAll() { + // TODO Auto-generated method stub + + } + + @Override + public List<Task> getAllPendingTasks() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/core/java/android/app/TimePickerDialog.java b/core/java/android/app/TimePickerDialog.java index a85c61f..8cf8c25 100644 --- a/core/java/android/app/TimePickerDialog.java +++ b/core/java/android/app/TimePickerDialog.java @@ -110,6 +110,7 @@ public class TimePickerDialog extends AlertDialog (LayoutInflater) themeContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); View view = inflater.inflate(R.layout.time_picker_dialog, null); setView(view); + setButtonPanelLayoutHint(LAYOUT_HINT_SIDE); mTimePicker = (TimePicker) view.findViewById(R.id.timePicker); // Initialize state diff --git a/core/java/android/app/UiAutomation.java b/core/java/android/app/UiAutomation.java index 9405325..64e3484 100644 --- a/core/java/android/app/UiAutomation.java +++ b/core/java/android/app/UiAutomation.java @@ -26,6 +26,7 @@ import android.graphics.Canvas; import android.graphics.Point; import android.hardware.display.DisplayManagerGlobal; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.SystemClock; import android.util.Log; @@ -40,7 +41,9 @@ import android.view.accessibility.AccessibilityInteractionClient; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityWindowInfo; import android.view.accessibility.IAccessibilityInteractionConnection; +import libcore.io.IoUtils; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeoutException; @@ -840,6 +843,44 @@ public final class UiAutomation { return null; } + /** + * Executes a shell command. This method returs a file descriptor that points + * to the standard output stream. The command execution is similar to running + * "adb shell <command>" from a host connected to the device. + * <p> + * <strong>Note:</strong> It is your responsibility to close the retunred file + * descriptor once you are done reading. + * </p> + * + * @param command The command to execute. + * @return A file descriptor to the standard output stream. + */ + public ParcelFileDescriptor executeShellCommand(String command) { + synchronized (mLock) { + throwIfNotConnectedLocked(); + } + + ParcelFileDescriptor source = null; + ParcelFileDescriptor sink = null; + + try { + ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); + source = pipe[0]; + sink = pipe[1]; + + // Calling out without a lock held. + mUiAutomationConnection.executeShellCommand(command, sink); + } catch (IOException ioe) { + Log.e(LOG_TAG, "Error executing shell command!", ioe); + } catch (RemoteException re) { + Log.e(LOG_TAG, "Error executing shell command!", re); + } finally { + IoUtils.closeQuietly(sink); + } + + return source; + } + 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 fa40286..81bcb39 100644 --- a/core/java/android/app/UiAutomationConnection.java +++ b/core/java/android/app/UiAutomationConnection.java @@ -23,6 +23,7 @@ import android.graphics.Bitmap; import android.hardware.input.InputManager; import android.os.Binder; import android.os.IBinder; +import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.RemoteException; import android.os.ServiceManager; @@ -33,6 +34,12 @@ import android.view.WindowAnimationFrameStats; import android.view.WindowContentFrameStats; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.IAccessibilityManager; +import libcore.io.IoUtils; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; /** * This is a remote object that is passed from the shell to an instrumentation @@ -50,8 +57,8 @@ 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 IAccessibilityManager mAccessibilityManager = IAccessibilityManager.Stub + .asInterface(ServiceManager.getService(Service.ACCESSIBILITY_SERVICE)); private final Object mLock = new Object(); @@ -220,6 +227,41 @@ public final class UiAutomationConnection extends IUiAutomationConnection.Stub { } @Override + public void executeShellCommand(String command, ParcelFileDescriptor sink) + throws RemoteException { + synchronized (mLock) { + throwIfCalledByNotTrustedUidLocked(); + throwIfShutdownLocked(); + throwIfNotConnectedLocked(); + } + + InputStream in = null; + OutputStream out = null; + + try { + java.lang.Process process = Runtime.getRuntime().exec(command); + + in = process.getInputStream(); + out = new FileOutputStream(sink.getFileDescriptor()); + + final byte[] buffer = new byte[8192]; + while (true) { + final int readByteCount = in.read(buffer); + if (readByteCount < 0) { + break; + } + out.write(buffer, 0, readByteCount); + } + } catch (IOException ioe) { + throw new RuntimeException("Error running shell command", ioe); + } finally { + IoUtils.closeQuietly(in); + IoUtils.closeQuietly(out); + IoUtils.closeQuietly(sink); + } + } + + @Override public void shutdown() { synchronized (mLock) { if (isConnectedLocked()) { diff --git a/core/java/android/app/VoiceInteractor.java b/core/java/android/app/VoiceInteractor.java index 6dc48b0..85e970c 100644 --- a/core/java/android/app/VoiceInteractor.java +++ b/core/java/android/app/VoiceInteractor.java @@ -30,18 +30,39 @@ import com.android.internal.app.IVoiceInteractorRequest; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; -import java.util.WeakHashMap; +import java.util.ArrayList; /** - * Interface for an {@link Activity} to interact with the user through voice. + * Interface for an {@link Activity} to interact with the user through voice. Use + * {@link android.app.Activity#getVoiceInteractor() Activity.getVoiceInteractor} + * to retrieve the interface, if the activity is currently involved in a voice interaction. + * + * <p>The voice interactor revolves around submitting voice interaction requests to the + * back-end voice interaction service that is working with the user. These requests are + * submitted with {@link #submitRequest}, providing a new instance of a + * {@link Request} subclass describing the type of operation to perform -- currently the + * possible requests are {@link ConfirmationRequest} and {@link CommandRequest}. + * + * <p>Once a request is submitted, the voice system will process it and eventually deliver + * the result to the request object. The application can cancel a pending request at any + * time. + * + * <p>The VoiceInteractor is integrated with Activity's state saving mechanism, so that + * if an activity is being restarted with retained state, it will retain the current + * VoiceInteractor and any outstanding requests. Because of this, you should always use + * {@link Request#getActivity() Request.getActivity} to get back to the activity of a + * request, rather than holding on to the activity instance yourself, either explicitly + * or implicitly through a non-static inner class. */ public class VoiceInteractor { static final String TAG = "VoiceInteractor"; static final boolean DEBUG = true; - final Context mContext; - final Activity mActivity; final IVoiceInteractor mInteractor; + + Context mContext; + Activity mActivity; + final HandlerCaller mHandlerCaller; final HandlerCaller.Callback mHandlerCallerCallback = new HandlerCaller.Callback() { @Override @@ -60,6 +81,16 @@ public class VoiceInteractor { request.clear(); } break; + case MSG_ABORT_VOICE_RESULT: + request = pullRequest((IVoiceInteractorRequest)args.arg1, true); + if (DEBUG) Log.d(TAG, "onAbortVoice: req=" + + ((IVoiceInteractorRequest)args.arg1).asBinder() + "/" + request + + " result=" + args.arg1); + if (request != null) { + ((AbortVoiceRequest)request).onAbortResult((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=" @@ -94,6 +125,12 @@ public class VoiceInteractor { } @Override + public void deliverAbortVoiceResult(IVoiceInteractorRequest request, Bundle result) { + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOO( + MSG_ABORT_VOICE_RESULT, request, result)); + } + + @Override public void deliverCommandResult(IVoiceInteractorRequest request, boolean complete, Bundle result) { mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageIOO( @@ -110,8 +147,9 @@ public class VoiceInteractor { 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; + static final int MSG_ABORT_VOICE_RESULT = 2; + static final int MSG_COMMAND_RESULT = 3; + static final int MSG_CANCEL_RESULT = 4; public static abstract class Request { IVoiceInteractorRequest mRequestInterface; @@ -140,6 +178,12 @@ public class VoiceInteractor { public void onCancel() { } + public void onAttached(Activity activity) { + } + + public void onDetached() { + } + void clear() { mRequestInterface = null; mContext = null; @@ -180,9 +224,42 @@ public class VoiceInteractor { IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, IVoiceInteractorCallback callback) throws RemoteException { - return interactor.startConfirmation(packageName, callback, mPrompt.toString(), mExtras); + return interactor.startConfirmation(packageName, callback, mPrompt, mExtras); + } + } + + public static class AbortVoiceRequest extends Request { + final CharSequence mMessage; + final Bundle mExtras; + + /** + * Reports that the current interaction can not be complete with voice, so the + * application will need to switch to a traditional input UI. Applications should + * only use this when they need to completely bail out of the voice interaction + * and switch to a traditional UI. When the response comes back, the voice + * system has handled the request and is ready to switch; at that point the application + * can start a new non-voice activity. Be sure when starting the new activity + * to use {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK + * Intent.FLAG_ACTIVITY_NEW_TASK} to keep the new activity out of the current voice + * interaction task. + * + * @param message Optional message to tell user about not being able to complete + * the interaction with voice. + * @param extras Additional optional information. + */ + public AbortVoiceRequest(CharSequence message, Bundle extras) { + mMessage = message; + mExtras = extras; } - } + + public void onAbortResult(Bundle result) { + } + + IVoiceInteractorRequest submit(IVoiceInteractor interactor, String packageName, + IVoiceInteractorCallback callback) throws RemoteException { + return interactor.startAbortVoice(packageName, callback, mMessage, mExtras); + } + } public static class CommandRequest extends Request { final String mCommand; @@ -220,11 +297,11 @@ public class VoiceInteractor { } } - VoiceInteractor(Context context, Activity activity, IVoiceInteractor interactor, + VoiceInteractor(IVoiceInteractor interactor, Context context, Activity activity, Looper looper) { + mInteractor = interactor; mContext = context; mActivity = activity; - mInteractor = interactor; mHandlerCaller = new HandlerCaller(context, looper, mHandlerCallerCallback, true); } @@ -238,6 +315,49 @@ public class VoiceInteractor { } } + private ArrayList<Request> makeRequestList() { + final int N = mActiveRequests.size(); + if (N < 1) { + return null; + } + ArrayList<Request> list = new ArrayList<Request>(N); + for (int i=0; i<N; i++) { + list.add(mActiveRequests.valueAt(i)); + } + return list; + } + + void attachActivity(Activity activity) { + if (mActivity == activity) { + return; + } + mContext = activity; + mActivity = activity; + ArrayList<Request> reqs = makeRequestList(); + if (reqs != null) { + for (int i=0; i<reqs.size(); i++) { + Request req = reqs.get(i); + req.mContext = activity; + req.mActivity = activity; + req.onAttached(activity); + } + } + } + + void detachActivity() { + ArrayList<Request> reqs = makeRequestList(); + if (reqs != null) { + for (int i=0; i<reqs.size(); i++) { + Request req = reqs.get(i); + req.onDetached(); + req.mActivity = null; + req.mContext = null; + } + } + mContext = null; + mActivity = null; + } + public boolean submitRequest(Request request) { try { IVoiceInteractorRequest ireq = request.submit(mInteractor, diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 58d707c..48ff5b6 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -911,6 +911,35 @@ public class WallpaperManager { */ public void suggestDesiredDimensions(int minimumWidth, int minimumHeight) { try { + /** + * The framework makes no attempt to limit the window size + * to the maximum texture size. Any window larger than this + * cannot be composited. + * + * Read maximum texture size from system property and scale down + * minimumWidth and minimumHeight accordingly. + */ + int maximumTextureSize; + try { + maximumTextureSize = SystemProperties.getInt("sys.max_texture_size", 0); + } catch (Exception e) { + maximumTextureSize = 0; + } + + if (maximumTextureSize > 0) { + if ((minimumWidth > maximumTextureSize) || + (minimumHeight > maximumTextureSize)) { + float aspect = (float)minimumHeight / (float)minimumWidth; + if (minimumWidth > minimumHeight) { + minimumWidth = maximumTextureSize; + minimumHeight = (int)((minimumWidth * aspect) + 0.5); + } else { + minimumHeight = maximumTextureSize; + minimumWidth = (int)((minimumHeight / aspect) + 0.5); + } + } + } + if (sGlobals.mService == null) { Log.w(TAG, "WallpaperService not running"); } else { diff --git a/core/java/android/app/admin/DeviceAdminReceiver.java b/core/java/android/app/admin/DeviceAdminReceiver.java index f9d9059..ee222a9 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -166,12 +166,11 @@ public class DeviceAdminReceiver extends BroadcastReceiver { /** * Broadcast Action: This broadcast is sent to the newly created profile when - * the provisioning of a managed profile has completed successfully. + * the provisioning of a managed profile has completed successfully. It is used in both the + * Profile Owner and the Device Owner provisioning. * - * <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>The broadcast is limited to the DeviceAdminReceiver component specified in the message + * that started the provisioning. It is also limited to the managed profile. * * <p>Input: Nothing.</p> * <p>Output: Nothing</p> diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 24bb2cc..24a354f 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -16,8 +16,6 @@ package android.app.admin; -import org.xmlpull.v1.XmlPullParserException; - import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.ComponentName; @@ -27,6 +25,7 @@ import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.RestrictionsManager; import android.os.Bundle; import android.os.Handler; import android.os.Process; @@ -34,11 +33,15 @@ import android.os.RemoteCallback; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; +import android.os.UserManager; +import android.provider.Settings; import android.service.trust.TrustAgentService; import android.util.Log; import com.android.org.conscrypt.TrustedCertificateStore; +import org.xmlpull.v1.XmlPullParserException; + import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.InetSocketAddress; @@ -101,6 +104,17 @@ public class DevicePolicyManager { = "android.app.action.ACTION_PROVISION_MANAGED_PROFILE"; /** + * A broadcast intent with this action can be sent to ManagedProvisionning to specify that the + * user has already consented to the creation of the managed profile. + * The intent must contain the extras + * {@link #EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME} and + * {@link #EXTRA_PROVISIONING_TOKEN} + * @hide + */ + public static final String ACTION_PROVISIONING_USER_HAS_CONSENTED + = "android.app.action.USER_HAS_CONSENTED"; + + /** * 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}. @@ -109,6 +123,13 @@ public class DevicePolicyManager { = "deviceAdminPackageName"; /** + * An int extra used to identify the consent of the user to create the managed profile. + * <p>Use with {@link #ACTION_PROVISION_MANAGED_PROFILE} + */ + public static final String EXTRA_PROVISIONING_TOKEN + = "android.app.extra.token"; + + /** * 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} @@ -174,15 +195,16 @@ public class DevicePolicyManager { public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD"; /** - * Flag for {@link #addForwardingIntentFilter}: the intents will forwarded to the primary user. + * Flag used by {@link #addCrossProfileIntentFilter} to allow access of certain intents from a + * managed profile to its parent. */ - public static int FLAG_TO_PRIMARY_USER = 0x0001; + public static int FLAG_PARENT_CAN_ACCESS_MANAGED = 0x0001; /** - * Flag for {@link #addForwardingIntentFilter}: the intents will be forwarded to the managed - * profile. + * Flag used by {@link #addCrossProfileIntentFilter} to allow access of certain intents from the + * parent to its managed profile. */ - public static int FLAG_TO_MANAGED_PROFILE = 0x0002; + public static int FLAG_MANAGED_CAN_ACCESS_PARENT = 0x0002; /** * Return true if the given administrator component is currently @@ -359,8 +381,8 @@ public class DevicePolicyManager { } /** - * Retrieve the current minimum password quality for all admins - * or a particular one. + * Retrieve the current minimum password quality for all admins of this user + * and its profiles or a particular one. * @param admin The name of the admin component to check, or null to aggregate * all admins. */ @@ -412,8 +434,8 @@ public class DevicePolicyManager { } /** - * Retrieve the current minimum password length for all admins - * or a particular one. + * Retrieve the current minimum password length for all admins of this + * user and its profiles or a particular one. * @param admin The name of the admin component to check, or null to aggregate * all admins. */ @@ -467,8 +489,9 @@ public class DevicePolicyManager { /** * Retrieve the current number of upper case letters required in the - * password for all admins or a particular one. This is the same value as - * set by {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)} + * password for all admins of this user and its profiles or a particular one. + * This is the same value as set by + * {#link {@link #setPasswordMinimumUpperCase(ComponentName, int)} * and only applies when the password quality is * {@link #PASSWORD_QUALITY_COMPLEX}. * @@ -527,8 +550,9 @@ public class DevicePolicyManager { /** * Retrieve the current number of lower case letters required in the - * password for all admins or a particular one. This is the same value as - * set by {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)} + * password for all admins of this user and its profiles or a particular one. + * This is the same value as set by + * {#link {@link #setPasswordMinimumLowerCase(ComponentName, int)} * and only applies when the password quality is * {@link #PASSWORD_QUALITY_COMPLEX}. * @@ -644,8 +668,9 @@ public class DevicePolicyManager { /** * Retrieve the current number of numerical digits required in the password - * for all admins or a particular one. This is the same value as - * set by {#link {@link #setPasswordMinimumNumeric(ComponentName, int)} + * for all admins of this user and its profiles or a particular one. + * This is the same value as set by + * {#link {@link #setPasswordMinimumNumeric(ComponentName, int)} * and only applies when the password quality is * {@link #PASSWORD_QUALITY_COMPLEX}. * @@ -760,8 +785,9 @@ public class DevicePolicyManager { /** * Retrieve the current number of non-letter characters required in the - * password for all admins or a particular one. This is the same value as - * set by {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)} + * password for all admins of this user and its profiles or a particular one. + * This is the same value as set by + * {#link {@link #setPasswordMinimumNonLetter(ComponentName, int)} * and only applies when the password quality is * {@link #PASSWORD_QUALITY_COMPLEX}. * @@ -868,9 +894,10 @@ public class DevicePolicyManager { /** * Get the current password expiration time for the given admin or an aggregate of - * all admins if admin is null. If the password is expired, this will return the time since - * the password expired as a negative number. If admin is null, then a composite of all - * expiration timeouts is returned - which will be the minimum of all timeouts. + * all admins of this user and its profiles if admin is null. If the password is + * expired, this will return the time since the password expired as a negative number. + * If admin is null, then a composite of all expiration timeouts is returned + * - which will be the minimum of all timeouts. * * @param admin The name of the admin component to check, or null to aggregate all admins. * @return The password expiration time, in ms. @@ -887,8 +914,8 @@ public class DevicePolicyManager { } /** - * Retrieve the current password history length for all admins - * or a particular one. + * Retrieve the current password history length for all admins of this + * user and its profiles or a particular one. * @param admin The name of the admin component to check, or null to aggregate * all admins. * @return The length of the password history @@ -923,14 +950,13 @@ public class DevicePolicyManager { /** * Determine whether the current password the user has set is sufficient * to meet the policy requirements (quality, minimum length) that have been - * requested. + * requested by the admins of this user and its profiles. * * <p>The calling device admin must have requested * {@link DeviceAdminInfo#USES_POLICY_LIMIT_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. * - * @return Returns true if the password meets the current requirements, - * else false. + * @return Returns true if the password meets the current requirements, else false. */ public boolean isActivePasswordSufficient() { if (mService != null) { @@ -993,7 +1019,7 @@ public class DevicePolicyManager { /** * Retrieve the current maximum number of login attempts that are allowed - * before the device wipes itself, for all admins + * before the device wipes itself, for all admins of this user and its profiles * or a particular one. * @param admin The name of the admin component to check, or null to aggregate * all admins. @@ -1037,6 +1063,8 @@ public class DevicePolicyManager { * {@link DeviceAdminInfo#USES_POLICY_RESET_PASSWORD} to be able to call * this method; if it has not, a security exception will be thrown. * + * Can not be called from a managed profile. + * * @param password The new password for the user. * @param flags May be 0 or {@link #RESET_PASSWORD_REQUIRE_ENTRY}. * @return Returns true if the password was applied, or false if it is @@ -1077,8 +1105,8 @@ public class DevicePolicyManager { } /** - * Retrieve the current maximum time to unlock for all admins - * or a particular one. + * Retrieve the current maximum time to unlock for all admins of this user + * and its profiles or a particular one. * @param admin The name of the admin component to check, or null to aggregate * all admins. */ @@ -1943,17 +1971,16 @@ public class DevicePolicyManager { } /** - * 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. + * Called by the profile owner so that some intents sent in the managed profile can also be + * resolved in the parent, or vice versa. * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param filter if an intent matches this IntentFilter, then it can be forwarded. + * @param filter The {@link IntentFilter} the intent has to match to be also resolved in the + * other profile */ - public void addForwardingIntentFilter(ComponentName admin, IntentFilter filter, int flags) { + public void addCrossProfileIntentFilter(ComponentName admin, IntentFilter filter, int flags) { if (mService != null) { try { - mService.addForwardingIntentFilter(admin, filter, flags); + mService.addCrossProfileIntentFilter(admin, filter, flags); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1961,14 +1988,14 @@ public class DevicePolicyManager { } /** - * Called by a profile owner to remove the forwarding intent filters from the current user - * and from the owner. + * Called by a profile owner to remove the cross-profile intent filters from the managed profile + * and from the parent. * @param admin Which {@link DeviceAdminReceiver} this request is associated with. */ - public void clearForwardingIntentFilters(ComponentName admin) { + public void clearCrossProfileIntentFilters(ComponentName admin) { if (mService != null) { try { - mService.clearForwardingIntentFilters(admin); + mService.clearCrossProfileIntentFilters(admin); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } @@ -1976,6 +2003,43 @@ public class DevicePolicyManager { } /** + * Called by a device owner to create a user with the specified name. The UserHandle returned + * by this method should not be persisted as user handles are recycled as users are removed and + * created. If you need to persist an identifier for this user, use + * {@link UserManager#getSerialNumberForUser}. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param name the user's name + * @see UserHandle + * @return the UserHandle object for the created user, or null if the user could not be created. + */ + public UserHandle createUser(ComponentName admin, String name) { + try { + return mService.createUser(admin, name); + } catch (RemoteException re) { + Log.w(TAG, "Could not create a user", re); + } + return null; + } + + /** + * Called by a device owner to remove a user and all associated data. The primary user can + * not be removed. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param userHandle the user to remove. + * @return {@code true} if the user was removed, {@code false} otherwise. + */ + public boolean removeUser(ComponentName admin, UserHandle userHandle) { + try { + return mService.removeUser(admin, userHandle); + } catch (RemoteException re) { + Log.w(TAG, "Could not remove user ", re); + return false; + } + } + + /** * Called by a profile or device owner to get the application restrictions for a given target * application running in the managed profile. * @@ -2044,64 +2108,90 @@ public class DevicePolicyManager { } /** - * Called by profile or device owner to re-enable a system app that was disabled by default - * when the managed profile was created. This should only be called from a profile or device - * owner running within a managed profile. + * Called by device or profile owner to block or unblock packages. When a package is blocked it + * is unavailable for use, but the data and actual package file remain. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param packageName The package to be re-enabled in the current profile. + * @param packageName The name of the package to block or unblock. + * @param blocked {@code true} if the package should be blocked, {@code false} if it should be + * unblocked. + * @return boolean Whether the blocked setting of the package was successfully updated. */ - public void enableSystemApp(ComponentName admin, String packageName) { + public boolean setApplicationBlocked(ComponentName admin, String packageName, + boolean blocked) { if (mService != null) { try { - mService.enableSystemApp(admin, packageName); + return mService.setApplicationBlocked(admin, packageName, blocked); } catch (RemoteException e) { - Log.w(TAG, "Failed to install package: " + packageName); + Log.w(TAG, "Failed talking with device policy service", e); } } + return false; } /** - * Called by a profile owner to disable account management for a specific type of account. + * Called by profile or device owner to block or unblock currently installed packages. This + * should only be called by a profile or device owner running within a managed profile. * - * <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 intent An intent matching the app(s) to be updated. All apps that resolve for this + * intent will be updated in the current profile. + * @param blocked {@code true} if the packages should be blocked, {@code false} if they should + * be unblocked. + * @return int The number of activities that matched the intent and were updated. + */ + public int setApplicationsBlocked(ComponentName admin, Intent intent, boolean blocked) { + if (mService != null) { + try { + return mService.setApplicationsBlocked(admin, intent, blocked); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + return 0; + } + + /** + * Called by device or profile owner to determine if a package is blocked. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param accountType For which account management is disabled or enabled. - * @param disabled The boolean indicating that account management will be disabled (true) or - * enabled (false). + * @param packageName The name of the package to retrieve the blocked status of. + * @return boolean {@code true} if the package is blocked, {@code false} otherwise. */ - public void setAccountManagementDisabled(ComponentName admin, String accountType, - boolean disabled) { + public boolean isApplicationBlocked(ComponentName admin, String packageName) { if (mService != null) { try { - mService.setAccountManagementDisabled(admin, accountType, disabled); + return mService.isApplicationBlocked(admin, packageName); } catch (RemoteException e) { Log.w(TAG, "Failed talking with device policy service", e); } } + return false; } /** - * Called by profile or device owner to re-enable system apps by intent that were disabled - * by default when the managed profile was created. This should only be called from a profile - * or device owner running within a managed profile. + * Called by a profile owner to disable account management for a specific type of account. + * + * <p>The calling device admin must be a profile owner. If it is not, a + * security exception will be thrown. + * + * <p>When account management is disabled for an account type, adding or removing an account + * of that type will not be possible. * * @param admin Which {@link DeviceAdminReceiver} this request is associated with. - * @param intent An intent matching the app(s) to be installed. All apps that resolve for this - * intent will be re-enabled in the current profile. - * @return int The number of activities that matched the intent and were installed. + * @param accountType For which account management is disabled or enabled. + * @param disabled The boolean indicating that account management will be disabled (true) or + * enabled (false). */ - public int enableSystemApp(ComponentName admin, Intent intent) { + public void setAccountManagementDisabled(ComponentName admin, String accountType, + boolean disabled) { if (mService != null) { try { - return mService.enableSystemAppWithIntent(admin, intent); + mService.setAccountManagementDisabled(admin, accountType, disabled); } catch (RemoteException e) { - Log.w(TAG, "Failed to install packages matching filter: " + intent); + Log.w(TAG, "Failed talking with device policy service", e); } } - return 0; } /** @@ -2172,4 +2262,61 @@ public class DevicePolicyManager { } return false; } + + /** + * Called by device owners to update {@link Settings.Global} settings. Validation that the value + * of the setting is in the correct form for the setting type should be performed by the caller. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param setting The name of the setting to update. + * @param value The value to update the setting to. + */ + public void setGlobalSetting(ComponentName admin, String setting, String value) { + if (mService != null) { + try { + mService.setGlobalSetting(admin, setting, value); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Called by profile or device owners to update {@link Settings.Secure} settings. Validation + * that the value of the setting is in the correct form for the setting type should be performed + * by the caller. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param setting The name of the setting to update. + * @param value The value to update the setting to. + */ + public void setSecureSetting(ComponentName admin, String setting, String value) { + if (mService != null) { + try { + mService.setSecureSetting(admin, setting, value); + } catch (RemoteException e) { + Log.w(TAG, "Failed talking with device policy service", e); + } + } + } + + /** + * Designates a specific broadcast receiver component as the provider for + * making permission requests of a local or remote administrator of the user. + * <p/> + * Only a profile owner can designate the restrictions provider. + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @param receiver The component name of a BroadcastReceiver that handles the + * {@link RestrictionsManager#ACTION_REQUEST_PERMISSION} intent. If this param is null, + * it removes the restrictions provider previously assigned. + */ + public void setRestrictionsProvider(ComponentName admin, ComponentName receiver) { + if (mService != null) { + try { + mService.setRestrictionsProvider(admin, receiver); + } catch (RemoteException re) { + Log.w(TAG, "Failed to set permission provider on device policy service"); + } + } + } } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index 03ced0f..7d7a312 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -22,6 +22,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.os.RemoteCallback; +import android.os.UserHandle; /** * Internal IPC interface to the device policy service. @@ -120,12 +121,19 @@ interface IDevicePolicyManager { void setApplicationRestrictions(in ComponentName who, in String packageName, in Bundle settings); Bundle getApplicationRestrictions(in ComponentName who, in String packageName); + void setRestrictionsProvider(in ComponentName who, in ComponentName provider); + ComponentName getRestrictionsProvider(int userHandle); + void setUserRestriction(in ComponentName who, in String key, boolean enable); - void addForwardingIntentFilter(in ComponentName admin, in IntentFilter filter, int flags); - void clearForwardingIntentFilters(in ComponentName admin); + void addCrossProfileIntentFilter(in ComponentName admin, in IntentFilter filter, int flags); + void clearCrossProfileIntentFilters(in ComponentName admin); + + boolean setApplicationBlocked(in ComponentName admin, in String packageName, boolean blocked); + int setApplicationsBlocked(in ComponentName admin, in Intent intent, boolean blocked); + boolean isApplicationBlocked(in ComponentName admin, in String packageName); - void enableSystemApp(in ComponentName admin, in String packageName); - int enableSystemAppWithIntent(in ComponentName admin, in Intent intent); + UserHandle createUser(in ComponentName who, in String name); + boolean removeUser(in ComponentName who, in UserHandle userHandle); void setAccountManagementDisabled(in ComponentName who, in String accountType, in boolean disabled); String[] getAccountTypesWithManagementDisabled(); @@ -133,4 +141,7 @@ interface IDevicePolicyManager { void setLockTaskComponents(in ComponentName[] components); ComponentName[] getLockTaskComponents(); boolean isLockTaskPermitted(in ComponentName component); + + void setGlobalSetting(in ComponentName who, in String setting, in String value); + void setSecureSetting(in ComponentName who, in String setting, in String value); } diff --git a/core/java/android/app/backup/BackupTransport.java b/core/java/android/app/backup/BackupTransport.java new file mode 100644 index 0000000..46f082e --- /dev/null +++ b/core/java/android/app/backup/BackupTransport.java @@ -0,0 +1,415 @@ +/* + * 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.backup; + +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +import com.android.internal.backup.IBackupTransport; + +/** + * Concrete class that provides a stable-API bridge between IBackupTransport + * and its implementations. + * + * @hide + */ +public class BackupTransport { + public static final int TRANSPORT_OK = 0; + public static final int TRANSPORT_ERROR = 1; + public static final int TRANSPORT_NOT_INITIALIZED = 2; + public static final int TRANSPORT_PACKAGE_REJECTED = 3; + public static final int AGENT_ERROR = 4; + public static final int AGENT_UNKNOWN = 5; + + IBackupTransport mBinderImpl = new TransportImpl(); + /** @hide */ + public IBinder getBinder() { + return mBinderImpl.asBinder(); + } + + // ------------------------------------------------------------------------------------ + // Transport self-description and general configuration interfaces + // + + /** + * Ask the transport for the name under which it should be registered. This will + * typically be its host service's component name, but need not be. + */ + public String name() { + throw new UnsupportedOperationException("Transport name() not implemented"); + } + + /** + * Ask the transport for an Intent that can be used to launch any internal + * configuration Activity that it wishes to present. For example, the transport + * may offer a UI for allowing the user to supply login credentials for the + * transport's off-device backend. + * + * If the transport does not supply any user-facing configuration UI, it should + * return null from this method. + * + * @return An Intent that can be passed to Context.startActivity() in order to + * launch the transport's configuration UI. This method will return null + * if the transport does not offer any user-facing configuration UI. + */ + public Intent configurationIntent() { + return null; + } + + /** + * On demand, supply a one-line string that can be shown to the user that + * describes the current backend destination. For example, a transport that + * can potentially associate backup data with arbitrary user accounts should + * include the name of the currently-active account here. + * + * @return A string describing the destination to which the transport is currently + * sending data. This method should not return null. + */ + public String currentDestinationString() { + throw new UnsupportedOperationException( + "Transport currentDestinationString() not implemented"); + } + + /** + * Ask the transport where, on local device storage, to keep backup state blobs. + * This is per-transport so that mock transports used for testing can coexist with + * "live" backup services without interfering with the live bookkeeping. The + * returned string should be a name that is expected to be unambiguous among all + * available backup transports; the name of the class implementing the transport + * is a good choice. + * + * @return A unique name, suitable for use as a file or directory name, that the + * Backup Manager could use to disambiguate state files associated with + * different backup transports. + */ + public String transportDirName() { + throw new UnsupportedOperationException( + "Transport transportDirName() not implemented"); + } + + // ------------------------------------------------------------------------------------ + // Device-level operations common to both key/value and full-data storage + + /** + * Initialize the server side storage for this device, erasing all stored data. + * The transport may send the request immediately, or may buffer it. After + * this is called, {@link #finishBackup} will be called to ensure the request + * is sent and received successfully. + * + * @return One of {@link BackupTransport#TRANSPORT_OK} (OK so far) or + * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure). + */ + public int initializeDevice() { + return BackupTransport.TRANSPORT_ERROR; + } + + /** + * Erase the given application's data from the backup destination. This clears + * out the given package's data from the current backup set, making it as though + * the app had never yet been backed up. After this is called, {@link finishBackup} + * must be called to ensure that the operation is recorded successfully. + * + * @return the same error codes as {@link #performBackup}. + */ + public int clearBackupData(PackageInfo packageInfo) { + return BackupTransport.TRANSPORT_ERROR; + } + + /** + * Finish sending application data to the backup destination. This must be + * called after {@link #performBackup}, {@link #performFullBackup}, or {@link clearBackupData} + * to ensure that all data is sent and the operation properly finalized. Only when this + * method returns true can a backup be assumed to have succeeded. + * + * @return the same error codes as {@link #performBackup} or {@link #performFullBackup}. + */ + public int finishBackup() { + return BackupTransport.TRANSPORT_ERROR; + } + + // ------------------------------------------------------------------------------------ + // Key/value incremental backup support interfaces + + /** + * Verify that this is a suitable time for a key/value backup pass. This should return zero + * if a backup is reasonable right now, some positive value otherwise. This method + * will be called outside of the {@link #performBackup}/{@link #finishBackup} pair. + * + * <p>If this is not a suitable time for a backup, the transport should return a + * backoff delay, in milliseconds, after which the Backup Manager should try again. + * + * @return Zero if this is a suitable time for a backup pass, or a positive time delay + * in milliseconds to suggest deferring the backup pass for a while. + */ + public long requestBackupTime() { + return 0; + } + + /** + * Send one application's key/value data update to the backup destination. The + * transport may send the data immediately, or may buffer it. After this is called, + * {@link #finishBackup} will be called to ensure the data is sent and recorded successfully. + * + * @param packageInfo The identity of the application whose data is being backed up. + * This specifically includes the signature list for the package. + * @param data The data stream that resulted from invoking the application's + * BackupService.doBackup() method. This may be a pipe rather than a file on + * persistent media, so it may not be seekable. + * @param wipeAllFirst When true, <i>all</i> backed-up data for the current device/account + * must be erased prior to the storage of the data provided here. The purpose of this + * is to provide a guarantee that no stale data exists in the restore set when the + * device begins providing incremental backups. + * @return one of {@link BackupTransport#TRANSPORT_OK} (OK so far), + * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), or + * {@link BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has + * become lost due to inactivity purge or some other reason and needs re-initializing) + */ + public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd) { + return BackupTransport.TRANSPORT_ERROR; + } + + // ------------------------------------------------------------------------------------ + // Key/value dataset restore interfaces + + /** + * Get the set of all backups currently available over this transport. + * + * @return Descriptions of the set of restore images available for this device, + * or null if an error occurred (the attempt should be rescheduled). + **/ + public RestoreSet[] getAvailableRestoreSets() { + return null; + } + + /** + * Get the identifying token of the backup set currently being stored from + * this device. This is used in the case of applications wishing to restore + * their last-known-good data. + * + * @return A token that can be passed to {@link #startRestore}, or 0 if there + * is no backup set available corresponding to the current device state. + */ + public long getCurrentRestoreSet() { + return 0; + } + + /** + * Start restoring application data from backup. After calling this function, + * alternate calls to {@link #nextRestorePackage} and {@link #nextRestoreData} + * to walk through the actual application data. + * + * @param token A backup token as returned by {@link #getAvailableRestoreSets} + * or {@link #getCurrentRestoreSet}. + * @param packages List of applications to restore (if data is available). + * Application data will be restored in the order given. + * @return One of {@link BackupTransport#TRANSPORT_OK} (OK so far, call + * {@link #nextRestorePackage}) or {@link BackupTransport#TRANSPORT_ERROR} + * (an error occurred, the restore should be aborted and rescheduled). + */ + public int startRestore(long token, PackageInfo[] packages) { + return BackupTransport.TRANSPORT_ERROR; + } + + /** + * Get the package name of the next application with data in the backup store. + * + * @return The name of one of the packages supplied to {@link #startRestore}, + * or "" (the empty string) if no more backup data is available, + * or null if an error occurred (the restore should be aborted and rescheduled). + */ + public String nextRestorePackage() { + return null; + } + + /** + * Get the data for the application returned by {@link #nextRestorePackage}. + * @param data An open, writable file into which the backup data should be stored. + * @return the same error codes as {@link #startRestore}. + */ + public int getRestoreData(ParcelFileDescriptor outFd) { + return BackupTransport.TRANSPORT_ERROR; + } + + /** + * End a restore session (aborting any in-process data transfer as necessary), + * freeing any resources and connections used during the restore process. + */ + public void finishRestore() { + throw new UnsupportedOperationException( + "Transport finishRestore() not implemented"); + } + + // ------------------------------------------------------------------------------------ + // Full backup interfaces + + /** + * Verify that this is a suitable time for a full-data backup pass. This should return zero + * if a backup is reasonable right now, some positive value otherwise. This method + * will be called outside of the {@link #performFullBackup}/{@link #finishBackup} pair. + * + * <p>If this is not a suitable time for a backup, the transport should return a + * backoff delay, in milliseconds, after which the Backup Manager should try again. + * + * @return Zero if this is a suitable time for a backup pass, or a positive time delay + * in milliseconds to suggest deferring the backup pass for a while. + * + * @see #requestBackupTime() + */ + public long requestFullBackupTime() { + return 0; + } + + /** + * Begin the process of sending an application's full-data archive to the backend. + * The description of the package whose data will be delivered is provided, as well as + * the socket file descriptor on which the transport will receive the data itself. + * + * <p>If the package is not eligible for backup, the transport should return + * {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED}. In this case the system will + * simply proceed with the next candidate if any, or finish the full backup operation + * if all apps have been processed. + * + * <p>After the transport returns {@link BackupTransport#TRANSPORT_OK} from this + * method, the OS will proceed to call {@link #sendBackupData()} one or more times + * to deliver the application's data as a streamed tarball. The transport should not + * read() from the socket except as instructed to via the {@link #sendBackupData(int)} + * method. + * + * <p>After all data has been delivered to the transport, the system will call + * {@link #finishBackup()}. At this point the transport should commit the data to + * its datastore, if appropriate, and close the socket that had been provided in + * {@link #performFullBackup(PackageInfo, ParcelFileDescriptor)}. + * + * @param targetPackage The package whose data is to follow. + * @param socket The socket file descriptor through which the data will be provided. + * If the transport returns {@link #TRANSPORT_PACKAGE_REJECTED} here, it must still + * close this file descriptor now; otherwise it should be cached for use during + * succeeding calls to {@link #sendBackupData(int)}, and closed in response to + * {@link #finishBackup()}. + * @return TRANSPORT_PACKAGE_REJECTED to indicate that the stated application is not + * to be backed up; TRANSPORT_OK to indicate that the OS may proceed with delivering + * backup data; TRANSPORT_ERROR to indicate a fatal error condition that precludes + * performing a backup at this time. + */ + public int performFullBackup(PackageInfo targetPackage, ParcelFileDescriptor socket) { + return BackupTransport.TRANSPORT_PACKAGE_REJECTED; + } + + /** + * Tells the transport to read {@code numBytes} bytes of data from the socket file + * descriptor provided in the {@link #performFullBackup(PackageInfo, ParcelFileDescriptor)} + * call, and deliver those bytes to the datastore. + * + * @param numBytes The number of bytes of tarball data available to be read from the + * socket. + * @return TRANSPORT_OK on successful processing of the data; TRANSPORT_ERROR to + * indicate a fatal error situation. If an error is returned, the system will + * call finishBackup() and stop attempting backups until after a backoff and retry + * interval. + */ + public int sendBackupData(int numBytes) { + return BackupTransport.TRANSPORT_ERROR; + } + + /** + * Bridge between the actual IBackupTransport implementation and the stable API. If the + * binder interface needs to change, we use this layer to translate so that we can + * (if appropriate) decouple those framework-side changes from the BackupTransport + * implementations. + */ + class TransportImpl extends IBackupTransport.Stub { + + @Override + public String name() throws RemoteException { + return BackupTransport.this.name(); + } + + @Override + public Intent configurationIntent() throws RemoteException { + return BackupTransport.this.configurationIntent(); + } + + @Override + public String currentDestinationString() throws RemoteException { + return BackupTransport.this.currentDestinationString(); + } + + @Override + public String transportDirName() throws RemoteException { + return BackupTransport.this.transportDirName(); + } + + @Override + public long requestBackupTime() throws RemoteException { + return BackupTransport.this.requestBackupTime(); + } + + @Override + public int initializeDevice() throws RemoteException { + return BackupTransport.this.initializeDevice(); + } + + @Override + public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd) + throws RemoteException { + return BackupTransport.this.performBackup(packageInfo, inFd); + } + + @Override + public int clearBackupData(PackageInfo packageInfo) throws RemoteException { + return BackupTransport.this.clearBackupData(packageInfo); + } + + @Override + public int finishBackup() throws RemoteException { + return BackupTransport.this.finishBackup(); + } + + @Override + public RestoreSet[] getAvailableRestoreSets() throws RemoteException { + return BackupTransport.this.getAvailableRestoreSets(); + } + + @Override + public long getCurrentRestoreSet() throws RemoteException { + return BackupTransport.this.getCurrentRestoreSet(); + } + + @Override + public int startRestore(long token, PackageInfo[] packages) throws RemoteException { + return BackupTransport.this.startRestore(token, packages); + } + + @Override + public String nextRestorePackage() throws RemoteException { + return BackupTransport.this.nextRestorePackage(); + } + + @Override + public int getRestoreData(ParcelFileDescriptor outFd) throws RemoteException { + return BackupTransport.this.getRestoreData(outFd); + } + + @Override + public void finishRestore() throws RemoteException { + BackupTransport.this.finishRestore(); + } + } +} diff --git a/core/java/android/app/task/ITaskCallback.aidl b/core/java/android/app/task/ITaskCallback.aidl index ffa57d1..d8a32fd 100644 --- a/core/java/android/app/task/ITaskCallback.aidl +++ b/core/java/android/app/task/ITaskCallback.aidl @@ -34,14 +34,17 @@ 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. + * @param ongoing True to indicate that the client is processing the task. False if the task is + * complete */ - void acknowledgeStartMessage(int taskId); + void acknowledgeStartMessage(int taskId, boolean ongoing); /** * Immediate callback to the system after sending a stop signal, used to quickly detect ANR. * * @param taskId Unique integer used to identify this task. + * @param rescheulde Whether or not to reschedule this task. */ - void acknowledgeStopMessage(int taskId); + void acknowledgeStopMessage(int taskId, boolean reschedule); /* * 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. diff --git a/core/java/android/app/task/ITaskManager.aidl b/core/java/android/app/task/ITaskManager.aidl new file mode 100644 index 0000000..b56c78a --- /dev/null +++ b/core/java/android/app/task/ITaskManager.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.app.task; + +import android.app.task.Task; + + /** + * IPC interface that supports the app-facing {@link #TaskManager} api. + * {@hide} + */ +interface ITaskManager { + int schedule(in Task task); + void cancel(int taskId); + void cancelAll(); + List<Task> getAllPendingTasks(); +} diff --git a/core/java/android/tv/TvInputInfo.aidl b/core/java/android/app/task/Task.aidl index abc4b47..1f25439 100644 --- a/core/java/android/tv/TvInputInfo.aidl +++ b/core/java/android/app/task/Task.aidl @@ -1,4 +1,4 @@ -/* +/** * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,7 @@ * limitations under the License. */ -package android.tv; +package android.app.task; -parcelable TvInputInfo; +parcelable Task; +
\ No newline at end of file diff --git a/core/java/android/content/Task.java b/core/java/android/app/task/Task.java index ed5ed88..ca4aeb2 100644 --- a/core/java/android/content/Task.java +++ b/core/java/android/app/task/Task.java @@ -14,23 +14,26 @@ * limitations under the License */ -package android.content; +package android.app.task; -import android.app.task.TaskService; +import android.content.ComponentName; 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 + * Container of data passed to the {@link android.app.task.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; + /** Default. */ + public final int NONE = 0; + /** This task requires network connectivity. */ + public final int ANY = 1; + /** This task requires network connectivity that is unmetered. */ + public final int UNMETERED = 2; } /** @@ -42,10 +45,26 @@ public class Task implements Parcelable { public final int EXPONENTIAL = 1; } + private final int taskId; + // TODO: Change this to use PersistableBundle when that lands in master. + private final Bundle extras; + private final ComponentName service; + private final boolean requireCharging; + private final boolean requireDeviceIdle; + private final boolean hasEarlyConstraint; + private final boolean hasLateConstraint; + 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; + /** * Unique task id associated with this class. This is assigned to your task by the scheduler. */ - public int getTaskId() { + public int getId() { return taskId; } @@ -59,8 +78,8 @@ public class Task implements Parcelable { /** * Name of the service endpoint that will be called back into by the TaskManager. */ - public String getServiceClassName() { - return serviceClassName; + public ComponentName getService() { + return service; } /** @@ -78,7 +97,7 @@ public class Task implements Parcelable { } /** - * See {@link android.content.Task.NetworkType} for a description of this value. + * See {@link android.app.task.Task.NetworkType} for a description of this value. */ public int getNetworkCapabilities() { return networkCapabilities; @@ -125,31 +144,35 @@ public class Task implements Parcelable { } /** - * See {@link android.content.Task.BackoffPolicy} for an explanation of the values this field + * See {@link android.app.task.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; + /** + * User can specify an early constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasEarlyConstraint() { + return hasEarlyConstraint; + } + + /** + * User can specify a late constraint of 0L, which is valid, so we keep track of whether the + * function was called at all. + * @hide + */ + public boolean hasLateConstraint() { + return hasLateConstraint; + } private Task(Parcel in) { taskId = in.readInt(); extras = in.readBundle(); - serviceClassName = in.readString(); + service = ComponentName.readFromParcel(in); requireCharging = in.readInt() == 1; requireDeviceIdle = in.readInt() == 1; networkCapabilities = in.readInt(); @@ -159,12 +182,14 @@ public class Task implements Parcelable { intervalMillis = in.readLong(); initialBackoffMillis = in.readLong(); backoffPolicy = in.readInt(); + hasEarlyConstraint = in.readInt() == 1; + hasLateConstraint = in.readInt() == 1; } private Task(Task.Builder b) { taskId = b.mTaskId; extras = new Bundle(b.mExtras); - serviceClassName = b.mTaskServiceClassName; + service = b.mTaskService; requireCharging = b.mRequiresCharging; requireDeviceIdle = b.mRequiresDeviceIdle; networkCapabilities = b.mNetworkCapabilities; @@ -174,6 +199,8 @@ public class Task implements Parcelable { intervalMillis = b.mIntervalMillis; initialBackoffMillis = b.mInitialBackoffMillis; backoffPolicy = b.mBackoffPolicy; + hasEarlyConstraint = b.mHasEarlyConstraint; + hasLateConstraint = b.mHasLateConstraint; } @Override @@ -185,7 +212,7 @@ public class Task implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeInt(taskId); out.writeBundle(extras); - out.writeString(serviceClassName); + ComponentName.writeToParcel(service, out); out.writeInt(requireCharging ? 1 : 0); out.writeInt(requireDeviceIdle ? 1 : 0); out.writeInt(networkCapabilities); @@ -195,6 +222,8 @@ public class Task implements Parcelable { out.writeLong(intervalMillis); out.writeLong(initialBackoffMillis); out.writeInt(backoffPolicy); + out.writeInt(hasEarlyConstraint ? 1 : 0); + out.writeInt(hasLateConstraint ? 1 : 0); } public static final Creator<Task> CREATOR = new Creator<Task>() { @@ -212,10 +241,10 @@ public class Task implements Parcelable { /** * Builder class for constructing {@link Task} objects. */ - public final class Builder { + public static final class Builder { private int mTaskId; private Bundle mExtras; - private String mTaskServiceClassName; + private ComponentName mTaskService; // Requirements. private boolean mRequiresCharging; private boolean mRequiresDeviceIdle; @@ -225,6 +254,8 @@ public class Task implements Parcelable { private long mMaxExecutionDelayMillis; // Periodic parameters. private boolean mIsPeriodic; + private boolean mHasEarlyConstraint; + private boolean mHasLateConstraint; private long mIntervalMillis; // Back-off parameters. private long mInitialBackoffMillis = 5000L; @@ -236,11 +267,11 @@ public class Task implements Parcelable { * @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 + * @param taskService The endpoint that you implement that will receive the callback from the * TaskManager. */ - public Builder(int taskId, Class<TaskService> cls) { - mTaskServiceClassName = cls.getClass().getName(); + public Builder(int taskId, ComponentName taskService) { + mTaskService = taskService; mTaskId = taskId; } @@ -255,7 +286,7 @@ public class Task implements Parcelable { /** * 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}. + * will be a parameter defined in {@link android.app.task.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 @@ -296,7 +327,7 @@ public class Task implements Parcelable { * 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 + * persisted beyond boot 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. @@ -307,6 +338,7 @@ public class Task implements Parcelable { public Builder setPeriodic(long intervalMillis) { mIsPeriodic = true; mIntervalMillis = intervalMillis; + mHasEarlyConstraint = mHasLateConstraint = true; return this; } @@ -314,12 +346,13 @@ public class Task implements Parcelable { * 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. + * {@link android.app.task.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; + mHasEarlyConstraint = true; return this; } @@ -328,10 +361,11 @@ public class Task implements Parcelable { * 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. + * {@link android.app.task.Task.Builder#build()} is called. */ public Builder setOverrideDeadline(long maxExecutionDelayMillis) { mMaxExecutionDelayMillis = maxExecutionDelayMillis; + mHasLateConstraint = true; return this; } @@ -360,31 +394,18 @@ public class Task implements Parcelable { * @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); + if (mExtras == null) { + mExtras = Bundle.EMPTY; + } + if (mTaskId < 0) { + throw new IllegalArgumentException("Task id must be greater than 0."); } // Check that a deadline was not set on a periodic task. - if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) { + if (mIsPeriodic && mHasLateConstraint) { throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + "periodic task."); } - if (mIsPeriodic && (mMinLatencyMillis != 0L)) { + if (mIsPeriodic && mHasEarlyConstraint) { throw new IllegalArgumentException("Can't call setMinimumLatency() on a " + "periodic task"); } diff --git a/core/java/android/content/TaskManager.java b/core/java/android/app/task/TaskManager.java index d28d78a..00f57da 100644 --- a/core/java/android/content/TaskManager.java +++ b/core/java/android/app/task/TaskManager.java @@ -14,14 +14,19 @@ * limitations under the License */ -package android.content; +package android.app.task; import java.util.List; +import android.content.Context; + /** * 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)}. + * <p>You do not + * instantiate this class directly; instead, retrieve it through + * {@link android.content.Context#getSystemService + * Context.getSystemService(Context.TASK_SERVICE)}. */ public abstract class TaskManager { /* @@ -29,18 +34,19 @@ public abstract class TaskManager { * 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; + public static final int RESULT_FAILURE = 0; /** * 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; + public static final int RESULT_SUCCESS = 1; - /* - * @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. + /** + * @param task The task you wish scheduled. See + * {@link android.app.task.Task.Builder Task.Builder} for more detail on the sorts of tasks + * you can schedule. + * @return If >0, this int returns 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); diff --git a/core/java/android/app/task/TaskParams.java b/core/java/android/app/task/TaskParams.java index e2eafd8..dacb348 100644 --- a/core/java/android/app/task/TaskParams.java +++ b/core/java/android/app/task/TaskParams.java @@ -29,7 +29,14 @@ public class TaskParams implements Parcelable { private final int taskId; private final Bundle extras; - private final IBinder mCallback; + private final IBinder callback; + + /** @hide */ + public TaskParams(int taskId, Bundle extras, IBinder callback) { + this.taskId = taskId; + this.extras = extras; + this.callback = callback; + } /** * @return The unique id of this task, specified at creation time. @@ -40,24 +47,22 @@ public class TaskParams implements Parcelable { /** * @return The extras you passed in when constructing this task with - * {@link android.content.Task.Builder#setExtras(android.os.Bundle)}. This will + * {@link android.app.task.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 - */ + /** @hide */ public ITaskCallback getCallback() { - return ITaskCallback.Stub.asInterface(mCallback); + return ITaskCallback.Stub.asInterface(callback); } private TaskParams(Parcel in) { taskId = in.readInt(); extras = in.readBundle(); - mCallback = in.readStrongBinder(); + callback = in.readStrongBinder(); } @Override @@ -69,7 +74,7 @@ public class TaskParams implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(taskId); dest.writeBundle(extras); - dest.writeStrongBinder(mCallback); + dest.writeStrongBinder(callback); } public static final Creator<TaskParams> CREATOR = new Creator<TaskParams>() { diff --git a/core/java/android/app/task/TaskService.java b/core/java/android/app/task/TaskService.java index 81333be..8ce4484 100644 --- a/core/java/android/app/task/TaskService.java +++ b/core/java/android/app/task/TaskService.java @@ -18,7 +18,6 @@ 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; @@ -29,7 +28,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; /** - * <p>Entry point for the callback from the {@link android.content.TaskManager}.</p> + * <p>Entry point for the callback from the {@link android.app.task.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> @@ -124,22 +123,20 @@ public abstract class TaskService extends Service { switch (msg.what) { case MSG_EXECUTE_TASK: try { - TaskService.this.onStartTask(params); + boolean workOngoing = TaskService.this.onStartTask(params); + ackStartMessage(params, workOngoing); } 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); + boolean ret = TaskService.this.onStopTask(params); + ackStopMessage(params, ret); } 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: @@ -162,30 +159,34 @@ public abstract class TaskService extends Service { } } - /** - * 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) { + private void ackStartMessage(TaskParams params, boolean workOngoing) { 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); - } + callback.acknowledgeStartMessage(taskId, workOngoing); } 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."); + Log.d(TAG, "Attempting to ack a task that has already been processed."); + } + } + } + + private void ackStopMessage(TaskParams params, boolean reschedule) { + final ITaskCallback callback = params.getCallback(); + final int taskId = params.getTaskId(); + if (callback != null) { + try { + callback.acknowledgeStopMessage(taskId, reschedule); + } catch(RemoteException e) { + Log.e(TAG, "System unreachable for stopping task."); + } + } else { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Attempting to ack a task that has already been processed."); } } } @@ -203,47 +204,43 @@ public abstract class TaskService extends Service { * * @param params Parameters specifying info about this task, including the extras bundle you * optionally provided at task-creation time. + * @return True if your service needs to process the work (on a separate thread). False if + * there's no more work to be done for this task. */ - public abstract void onStartTask(TaskParams params); + public abstract boolean onStartTask(TaskParams params); /** - * This method is called if your task should be stopped even before you've called - * {@link #taskFinished(TaskParams, boolean)}. + * This method is called if the system has determined that you must stop execution of your task + * even before you've had a chance to call {@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 + * {@link android.app.task.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 + * {@link android.app.task.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> + * immediate repercussion is that the system will cease holding a wakelock for you.</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. + * on the retry 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 + * Callback to inform the TaskManager you've finished executing. 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. + * 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.app.task.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 diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d3e9089..e5bf7d0 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -317,9 +317,9 @@ 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. + * Sent to an {@link AppWidgetProvider} after AppWidget state related to that 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: * @@ -343,7 +343,7 @@ public class AppWidgetManager { * <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 + * @see #ACTION_APPWIDGET_HOST_RESTORED */ public static final String ACTION_APPWIDGET_RESTORED = "android.appwidget.action.APPWIDGET_RESTORED"; @@ -352,7 +352,7 @@ public class AppWidgetManager { * 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. + * widget host instances, it will receive this broadcast separately for each one. * * <p>The intent will contain the following extras: * @@ -380,7 +380,7 @@ public class AppWidgetManager { * <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 + * @see #ACTION_APPWIDGET_RESTORED */ public static final String ACTION_APPWIDGET_HOST_RESTORED = "android.appwidget.action.APPWIDGET_HOST_RESTORED"; diff --git a/core/java/android/bluetooth/BluetoothAdapter.java b/core/java/android/bluetooth/BluetoothAdapter.java index e79deec..42c2aeb 100644 --- a/core/java/android/bluetooth/BluetoothAdapter.java +++ b/core/java/android/bluetooth/BluetoothAdapter.java @@ -18,6 +18,8 @@ package android.bluetooth; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.bluetooth.le.BluetoothLeAdvertiser; +import android.bluetooth.le.BluetoothLeScanner; import android.content.Context; import android.os.Handler; import android.os.IBinder; @@ -498,6 +500,34 @@ public final class BluetoothAdapter { } /** + * Returns a {@link BluetoothLeAdvertiser} object for Bluetooth LE Advertising operations. + */ + public BluetoothLeAdvertiser getBluetoothLeAdvertiser() { + // TODO: Return null if this feature is not supported by hardware. + try { + IBluetoothGatt iGatt = mManagerService.getBluetoothGatt(); + return new BluetoothLeAdvertiser(iGatt); + } catch (RemoteException e) { + Log.e(TAG, "failed to get BluetoothLeAdvertiser, error: " + e); + return null; + } + } + + /** + * Returns a {@link BluetoothLeScanner} object for Bluetooth LE scan operations. + */ + public BluetoothLeScanner getBluetoothLeScanner() { + // TODO: Return null if BLE scan is not supported by hardware. + try { + IBluetoothGatt iGatt = mManagerService.getBluetoothGatt(); + return new BluetoothLeScanner(iGatt); + } catch (RemoteException e) { + Log.e(TAG, "failed to get BluetoothLeScanner, error: " + e); + return null; + } + } + + /** * Interface for BLE advertising callback. * * @hide @@ -2024,6 +2054,10 @@ public final class BluetoothAdapter { } } + @Override + public void onMultiAdvertiseCallback(int status) { + // no op + } /** * Callback reporting LE ATT MTU. * @hide diff --git a/core/java/android/bluetooth/BluetoothGatt.java b/core/java/android/bluetooth/BluetoothGatt.java index 601d9ee..c9df9c0 100644 --- a/core/java/android/bluetooth/BluetoothGatt.java +++ b/core/java/android/bluetooth/BluetoothGatt.java @@ -581,7 +581,15 @@ public final class BluetoothGatt implements BluetoothProfile { public void onAdvertiseStateChange(int state, int status) { if (DBG) Log.d(TAG, "onAdvertiseStateChange() - state = " + state + " status=" + status); - } + } + + /** + * @hide + */ + @Override + public void onMultiAdvertiseCallback(int status) { + // no op. + } /** * Callback invoked when the MTU for a given connection changes diff --git a/core/java/android/bluetooth/BluetoothProfile.java b/core/java/android/bluetooth/BluetoothProfile.java index 1574090..d898060 100644 --- a/core/java/android/bluetooth/BluetoothProfile.java +++ b/core/java/android/bluetooth/BluetoothProfile.java @@ -104,6 +104,12 @@ public interface BluetoothProfile { public static final int MAP = 9; /** + * A2DP Sink Profile + * @hide + */ + public static final int A2DP_SINK = 10; + + /** * Default priority for devices that we try to auto-connect to and * and allow incoming connections for the profile * @hide diff --git a/core/java/android/bluetooth/BluetoothSocket.java b/core/java/android/bluetooth/BluetoothSocket.java index 00fd7ce..b98e5ae 100644 --- a/core/java/android/bluetooth/BluetoothSocket.java +++ b/core/java/android/bluetooth/BluetoothSocket.java @@ -81,8 +81,8 @@ import java.nio.ByteBuffer; */ public final class BluetoothSocket implements Closeable { private static final String TAG = "BluetoothSocket"; - private static final boolean DBG = true; - private static final boolean VDBG = false; + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean VDBG = Log.isLoggable(TAG, Log.VERBOSE); /** @hide */ public static final int MAX_RFCOMM_CHANNEL = 30; @@ -185,7 +185,7 @@ public final class BluetoothSocket implements Closeable { BluetoothSocket as = new BluetoothSocket(this); as.mSocketState = SocketState.CONNECTED; FileDescriptor[] fds = mSocket.getAncillaryFileDescriptors(); - if (VDBG) Log.d(TAG, "socket fd passed by stack fds: " + fds); + if (DBG) Log.d(TAG, "socket fd passed by stack fds: " + fds); if(fds == null || fds.length != 1) { Log.e(TAG, "socket fd passed from stack failed, fds: " + fds); as.close(); @@ -352,24 +352,24 @@ public final class BluetoothSocket implements Closeable { // read out port number try { synchronized(this) { - if (VDBG) Log.d(TAG, "bindListen(), SocketState: " + mSocketState + ", mPfd: " + + if (DBG) Log.d(TAG, "bindListen(), SocketState: " + mSocketState + ", mPfd: " + mPfd); if(mSocketState != SocketState.INIT) return EBADFD; if(mPfd == null) return -1; FileDescriptor fd = mPfd.getFileDescriptor(); - if (VDBG) Log.d(TAG, "bindListen(), new LocalSocket "); + if (DBG) Log.d(TAG, "bindListen(), new LocalSocket "); mSocket = new LocalSocket(fd); - if (VDBG) Log.d(TAG, "bindListen(), new LocalSocket.getInputStream() "); + if (DBG) Log.d(TAG, "bindListen(), new LocalSocket.getInputStream() "); mSocketIS = mSocket.getInputStream(); mSocketOS = mSocket.getOutputStream(); } - if (VDBG) Log.d(TAG, "bindListen(), readInt mSocketIS: " + mSocketIS); + if (DBG) Log.d(TAG, "bindListen(), readInt mSocketIS: " + mSocketIS); int channel = readInt(mSocketIS); synchronized(this) { if(mSocketState == SocketState.INIT) mSocketState = SocketState.LISTENING; } - if (VDBG) Log.d(TAG, "channel: " + channel); + if (DBG) Log.d(TAG, "channel: " + channel); if (mPort == -1) { mPort = channel; } // else ASSERT(mPort == channel) @@ -439,7 +439,7 @@ public final class BluetoothSocket implements Closeable { @Override public void close() throws IOException { - if (VDBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + mSocketState); + if (DBG) Log.d(TAG, "close() in, this: " + this + ", channel: " + mPort + ", state: " + mSocketState); if(mSocketState == SocketState.CLOSED) return; else @@ -449,10 +449,10 @@ public final class BluetoothSocket implements Closeable { if(mSocketState == SocketState.CLOSED) return; mSocketState = SocketState.CLOSED; - if (VDBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + ", mSocketIS: " + mSocketIS + + if (DBG) Log.d(TAG, "close() this: " + this + ", channel: " + mPort + ", mSocketIS: " + mSocketIS + ", mSocketOS: " + mSocketOS + "mSocket: " + mSocket); if(mSocket != null) { - if (VDBG) Log.d(TAG, "Closing mSocket: " + mSocket); + if (DBG) Log.d(TAG, "Closing mSocket: " + mSocket); mSocket.shutdownInput(); mSocket.shutdownOutput(); mSocket.close(); diff --git a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java index a0b603e..f0c8299 100644 --- a/core/java/android/bluetooth/BluetoothTetheringDataTracker.java +++ b/core/java/android/bluetooth/BluetoothTetheringDataTracker.java @@ -20,7 +20,7 @@ import android.net.BaseNetworkStateTracker; import android.content.Context; import android.net.ConnectivityManager; import android.net.DhcpResults; -import android.net.LinkCapabilities; +import android.net.NetworkCapabilities; import android.net.LinkProperties; import android.net.NetworkInfo; import android.net.NetworkInfo.DetailedState; @@ -75,7 +75,7 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { private BluetoothTetheringDataTracker() { mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_BLUETOOTH, 0, NETWORKTYPE, ""); mLinkProperties = new LinkProperties(); - mLinkCapabilities = new LinkCapabilities(); + mNetworkCapabilities = new NetworkCapabilities(); mNetworkInfo.setIsAvailable(false); setTeardownRequested(false); @@ -242,16 +242,6 @@ public class BluetoothTetheringDataTracker extends BaseNetworkStateTracker { } } - /** - * A capability is an Integer/String pair, the capabilities - * are defined in the class LinkSocket#Key. - * - * @return a copy of this connections capabilities, may be empty but never null. - */ - public LinkCapabilities getLinkCapabilities() { - return new LinkCapabilities(mLinkCapabilities); - } - /** * Fetch default gateway address for the network */ diff --git a/core/java/android/bluetooth/BluetoothUuid.java b/core/java/android/bluetooth/BluetoothUuid.java index ab53fb0..1e22eb3 100644 --- a/core/java/android/bluetooth/BluetoothUuid.java +++ b/core/java/android/bluetooth/BluetoothUuid.java @@ -273,11 +273,29 @@ public final class BluetoothUuid { * @param parcelUuid * @return true if the parcelUuid can be converted to 16 bit uuid, false otherwise. */ - public static boolean isShortUuid(ParcelUuid parcelUuid) { + public static boolean is16BitUuid(ParcelUuid parcelUuid) { UUID uuid = parcelUuid.getUuid(); if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) { return false; } return ((uuid.getMostSignificantBits() & 0xFFFF0000FFFFFFFFL) == 0x1000L); } + + + /** + * Check whether the given parcelUuid can be converted to 32 bit bluetooth uuid. + * + * @param parcelUuid + * @return true if the parcelUuid can be converted to 32 bit uuid, false otherwise. + */ + public static boolean is32BitUuid(ParcelUuid parcelUuid) { + UUID uuid = parcelUuid.getUuid(); + if (uuid.getLeastSignificantBits() != BASE_UUID.getUuid().getLeastSignificantBits()) { + return false; + } + if (is16BitUuid(parcelUuid)) { + return false; + } + return ((uuid.getMostSignificantBits() & 0xFFFFFFFFL) == 0x1000L); + } } diff --git a/core/java/android/bluetooth/IBluetoothGatt.aidl b/core/java/android/bluetooth/IBluetoothGatt.aidl index 49b156d..00a0750 100644 --- a/core/java/android/bluetooth/IBluetoothGatt.aidl +++ b/core/java/android/bluetooth/IBluetoothGatt.aidl @@ -17,6 +17,10 @@ package android.bluetooth; import android.bluetooth.BluetoothDevice; +import android.bluetooth.le.AdvertiseSettings; +import android.bluetooth.le.AdvertisementData; +import android.bluetooth.le.ScanFilter; +import android.bluetooth.le.ScanSettings; import android.os.ParcelUuid; import android.bluetooth.IBluetoothGattCallback; @@ -31,8 +35,16 @@ interface IBluetoothGatt { void startScan(in int appIf, in boolean isServer); void startScanWithUuids(in int appIf, in boolean isServer, in ParcelUuid[] ids); + void startScanWithUuidsScanParam(in int appIf, in boolean isServer, + in ParcelUuid[] ids, int scanWindow, int scanInterval); + void startScanWithFilters(in int appIf, in boolean isServer, + in ScanSettings settings, in List<ScanFilter> filters); void stopScan(in int appIf, in boolean isServer); - + void startMultiAdvertising(in int appIf, + in AdvertisementData advertiseData, + in AdvertisementData scanResponse, + in AdvertiseSettings settings); + void stopMultiAdvertising(in int appIf); void registerClient(in ParcelUuid appId, in IBluetoothGattCallback callback); void unregisterClient(in int clientIf); void clientConnect(in int clientIf, in String address, in boolean isDirect, in int transport); diff --git a/core/java/android/bluetooth/IBluetoothGattCallback.aidl b/core/java/android/bluetooth/IBluetoothGattCallback.aidl index a78c29b..bf9e0a7 100644 --- a/core/java/android/bluetooth/IBluetoothGattCallback.aidl +++ b/core/java/android/bluetooth/IBluetoothGattCallback.aidl @@ -64,5 +64,6 @@ interface IBluetoothGattCallback { in byte[] value); void onReadRemoteRssi(in String address, in int rssi, in int status); oneway void onAdvertiseStateChange(in int advertiseState, in int status); + oneway void onMultiAdvertiseCallback(in int status); void onConfigureMTU(in String address, in int mtu, in int status); } diff --git a/core/java/android/bluetooth/le/AdvertiseCallback.java b/core/java/android/bluetooth/le/AdvertiseCallback.java new file mode 100644 index 0000000..f1334c2 --- /dev/null +++ b/core/java/android/bluetooth/le/AdvertiseCallback.java @@ -0,0 +1,68 @@ +/* + * 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.bluetooth.le; + +/** + * Callback of Bluetooth LE advertising, which is used to deliver advertising operation status. + */ +public abstract class AdvertiseCallback { + + /** + * The operation is success. + * + * @hide + */ + public static final int SUCCESS = 0; + /** + * Fails to start advertising as the advertisement data contains services that are not added to + * the local bluetooth GATT server. + */ + public static final int ADVERTISE_FAILED_SERVICE_UNKNOWN = 1; + /** + * Fails to start advertising as system runs out of quota for advertisers. + */ + public static final int ADVERTISE_FAILED_TOO_MANY_ADVERTISERS = 2; + + /** + * Fails to start advertising as the advertising is already started. + */ + public static final int ADVERTISE_FAILED_ALREADY_STARTED = 3; + /** + * Fails to stop advertising as the advertising is not started. + */ + public static final int ADVERTISE_FAILED_NOT_STARTED = 4; + + /** + * Operation fails due to bluetooth controller failure. + */ + public static final int ADVERTISE_FAILED_CONTROLLER_FAILURE = 5; + + /** + * Callback when advertising operation succeeds. + * + * @param settingsInEffect The actual settings used for advertising, which may be different from + * what the app asks. + */ + public abstract void onSuccess(AdvertiseSettings settingsInEffect); + + /** + * Callback when advertising operation fails. + * + * @param errorCode Error code for failures. + */ + public abstract void onFailure(int errorCode); +} diff --git a/core/java/android/bluetooth/le/AdvertiseSettings.aidl b/core/java/android/bluetooth/le/AdvertiseSettings.aidl new file mode 100644 index 0000000..9f47d74 --- /dev/null +++ b/core/java/android/bluetooth/le/AdvertiseSettings.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.bluetooth.le; + +parcelable AdvertiseSettings;
\ No newline at end of file diff --git a/core/java/android/bluetooth/le/AdvertiseSettings.java b/core/java/android/bluetooth/le/AdvertiseSettings.java new file mode 100644 index 0000000..87d0346 --- /dev/null +++ b/core/java/android/bluetooth/le/AdvertiseSettings.java @@ -0,0 +1,218 @@ +/* + * 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.bluetooth.le; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * The {@link AdvertiseSettings} provide a way to adjust advertising preferences for each + * individual advertisement. Use {@link AdvertiseSettings.Builder} to create an instance. + */ +public final class AdvertiseSettings implements Parcelable { + /** + * Perform Bluetooth LE advertising in low power mode. This is the default and preferred + * advertising mode as it consumes the least power. + */ + public static final int ADVERTISE_MODE_LOW_POWER = 0; + /** + * Perform Bluetooth LE advertising in balanced power mode. This is balanced between advertising + * frequency and power consumption. + */ + public static final int ADVERTISE_MODE_BALANCED = 1; + /** + * Perform Bluetooth LE advertising in low latency, high power mode. This has the highest power + * consumption and should not be used for background continuous advertising. + */ + public static final int ADVERTISE_MODE_LOW_LATENCY = 2; + + /** + * Advertise using the lowest transmission(tx) power level. An app can use low transmission + * power to restrict the visibility range of its advertising packet. + */ + public static final int ADVERTISE_TX_POWER_ULTRA_LOW = 0; + /** + * Advertise using low tx power level. + */ + public static final int ADVERTISE_TX_POWER_LOW = 1; + /** + * Advertise using medium tx power level. + */ + public static final int ADVERTISE_TX_POWER_MEDIUM = 2; + /** + * Advertise using high tx power level. This is corresponding to largest visibility range of the + * advertising packet. + */ + public static final int ADVERTISE_TX_POWER_HIGH = 3; + + /** + * Non-connectable undirected advertising event, as defined in Bluetooth Specification V4.1 + * vol6, part B, section 4.4.2 - Advertising state. + */ + public static final int ADVERTISE_TYPE_NON_CONNECTABLE = 0; + /** + * Scannable undirected advertise type, as defined in same spec mentioned above. This event type + * allows a scanner to send a scan request asking additional information about the advertiser. + */ + public static final int ADVERTISE_TYPE_SCANNABLE = 1; + /** + * Connectable undirected advertising type, as defined in same spec mentioned above. This event + * type allows a scanner to send scan request asking additional information about the + * advertiser. It also allows an initiator to send a connect request for connection. + */ + public static final int ADVERTISE_TYPE_CONNECTABLE = 2; + + private final int mAdvertiseMode; + private final int mAdvertiseTxPowerLevel; + private final int mAdvertiseEventType; + + private AdvertiseSettings(int advertiseMode, int advertiseTxPowerLevel, + int advertiseEventType) { + mAdvertiseMode = advertiseMode; + mAdvertiseTxPowerLevel = advertiseTxPowerLevel; + mAdvertiseEventType = advertiseEventType; + } + + private AdvertiseSettings(Parcel in) { + mAdvertiseMode = in.readInt(); + mAdvertiseTxPowerLevel = in.readInt(); + mAdvertiseEventType = in.readInt(); + } + + /** + * Returns the advertise mode. + */ + public int getMode() { + return mAdvertiseMode; + } + + /** + * Returns the tx power level for advertising. + */ + public int getTxPowerLevel() { + return mAdvertiseTxPowerLevel; + } + + /** + * Returns the advertise event type. + */ + public int getType() { + return mAdvertiseEventType; + } + + @Override + public String toString() { + return "Settings [mAdvertiseMode=" + mAdvertiseMode + ", mAdvertiseTxPowerLevel=" + + mAdvertiseTxPowerLevel + ", mAdvertiseEventType=" + mAdvertiseEventType + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mAdvertiseMode); + dest.writeInt(mAdvertiseTxPowerLevel); + dest.writeInt(mAdvertiseEventType); + } + + public static final Parcelable.Creator<AdvertiseSettings> CREATOR = + new Creator<AdvertiseSettings>() { + @Override + public AdvertiseSettings[] newArray(int size) { + return new AdvertiseSettings[size]; + } + + @Override + public AdvertiseSettings createFromParcel(Parcel in) { + return new AdvertiseSettings(in); + } + }; + + /** + * Builder class for {@link AdvertiseSettings}. + */ + public static final class Builder { + private int mMode = ADVERTISE_MODE_LOW_POWER; + private int mTxPowerLevel = ADVERTISE_TX_POWER_MEDIUM; + private int mType = ADVERTISE_TYPE_NON_CONNECTABLE; + + /** + * Set advertise mode to control the advertising power and latency. + * + * @param advertiseMode Bluetooth LE Advertising mode, can only be one of + * {@link AdvertiseSettings#ADVERTISE_MODE_LOW_POWER}, + * {@link AdvertiseSettings#ADVERTISE_MODE_BALANCED}, or + * {@link AdvertiseSettings#ADVERTISE_MODE_LOW_LATENCY}. + * @throws IllegalArgumentException If the advertiseMode is invalid. + */ + public Builder setAdvertiseMode(int advertiseMode) { + if (advertiseMode < ADVERTISE_MODE_LOW_POWER + || advertiseMode > ADVERTISE_MODE_LOW_LATENCY) { + throw new IllegalArgumentException("unknown mode " + advertiseMode); + } + mMode = advertiseMode; + return this; + } + + /** + * Set advertise tx power level to control the transmission power level for the advertising. + * + * @param txPowerLevel Transmission power of Bluetooth LE Advertising, can only be one of + * {@link AdvertiseSettings#ADVERTISE_TX_POWER_ULTRA_LOW}, + * {@link AdvertiseSettings#ADVERTISE_TX_POWER_LOW}, + * {@link AdvertiseSettings#ADVERTISE_TX_POWER_MEDIUM} or + * {@link AdvertiseSettings#ADVERTISE_TX_POWER_HIGH}. + * @throws IllegalArgumentException If the {@code txPowerLevel} is invalid. + */ + public Builder setTxPowerLevel(int txPowerLevel) { + if (txPowerLevel < ADVERTISE_TX_POWER_ULTRA_LOW + || txPowerLevel > ADVERTISE_TX_POWER_HIGH) { + throw new IllegalArgumentException("unknown tx power level " + txPowerLevel); + } + mTxPowerLevel = txPowerLevel; + return this; + } + + /** + * Set advertise type to control the event type of advertising. + * + * @param type Bluetooth LE Advertising type, can be either + * {@link AdvertiseSettings#ADVERTISE_TYPE_NON_CONNECTABLE}, + * {@link AdvertiseSettings#ADVERTISE_TYPE_SCANNABLE} or + * {@link AdvertiseSettings#ADVERTISE_TYPE_CONNECTABLE}. + * @throws IllegalArgumentException If the {@code type} is invalid. + */ + public Builder setType(int type) { + if (type < ADVERTISE_TYPE_NON_CONNECTABLE + || type > ADVERTISE_TYPE_CONNECTABLE) { + throw new IllegalArgumentException("unknown advertise type " + type); + } + mType = type; + return this; + } + + /** + * Build the {@link AdvertiseSettings} object. + */ + public AdvertiseSettings build() { + return new AdvertiseSettings(mMode, mTxPowerLevel, mType); + } + } +} diff --git a/core/java/android/bluetooth/le/AdvertisementData.aidl b/core/java/android/bluetooth/le/AdvertisementData.aidl new file mode 100644 index 0000000..3da1321 --- /dev/null +++ b/core/java/android/bluetooth/le/AdvertisementData.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.bluetooth.le; + +parcelable AdvertisementData;
\ No newline at end of file diff --git a/core/java/android/bluetooth/le/AdvertisementData.java b/core/java/android/bluetooth/le/AdvertisementData.java new file mode 100644 index 0000000..c587204 --- /dev/null +++ b/core/java/android/bluetooth/le/AdvertisementData.java @@ -0,0 +1,344 @@ +/* + * 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.bluetooth.le; + +import android.annotation.Nullable; +import android.bluetooth.BluetoothUuid; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Advertisement data packet for Bluetooth LE advertising. This represents the data to be + * broadcasted in Bluetooth LE advertising as well as the scan response for active scan. + * <p> + * Use {@link AdvertisementData.Builder} to create an instance of {@link AdvertisementData} to be + * advertised. + * + * @see BluetoothLeAdvertiser + * @see ScanRecord + */ +public final class AdvertisementData implements Parcelable { + + @Nullable + private final List<ParcelUuid> mServiceUuids; + + private final int mManufacturerId; + @Nullable + private final byte[] mManufacturerSpecificData; + + @Nullable + private final ParcelUuid mServiceDataUuid; + @Nullable + private final byte[] mServiceData; + + private boolean mIncludeTxPowerLevel; + + private AdvertisementData(List<ParcelUuid> serviceUuids, + ParcelUuid serviceDataUuid, byte[] serviceData, + int manufacturerId, + byte[] manufacturerSpecificData, boolean includeTxPowerLevel) { + mServiceUuids = serviceUuids; + mManufacturerId = manufacturerId; + mManufacturerSpecificData = manufacturerSpecificData; + mServiceDataUuid = serviceDataUuid; + mServiceData = serviceData; + mIncludeTxPowerLevel = includeTxPowerLevel; + } + + /** + * Returns a list of service uuids within the advertisement that are used to identify the + * bluetooth GATT services. + */ + public List<ParcelUuid> getServiceUuids() { + return mServiceUuids; + } + + /** + * Returns the manufacturer identifier, which is a non-negative number assigned by Bluetooth + * SIG. + */ + public int getManufacturerId() { + return mManufacturerId; + } + + /** + * Returns the manufacturer specific data which is the content of manufacturer specific data + * field. The first 2 bytes of the data contain the company id. + */ + public byte[] getManufacturerSpecificData() { + return mManufacturerSpecificData; + } + + /** + * Returns a 16 bit uuid of the service that the service data is associated with. + */ + public ParcelUuid getServiceDataUuid() { + return mServiceDataUuid; + } + + /** + * Returns service data. The first two bytes should be a 16 bit service uuid associated with the + * service data. + */ + public byte[] getServiceData() { + return mServiceData; + } + + /** + * Whether the transmission power level will be included in the advertisement packet. + */ + public boolean getIncludeTxPowerLevel() { + return mIncludeTxPowerLevel; + } + + @Override + public String toString() { + return "AdvertisementData [mServiceUuids=" + mServiceUuids + ", mManufacturerId=" + + mManufacturerId + ", mManufacturerSpecificData=" + + Arrays.toString(mManufacturerSpecificData) + ", mServiceDataUuid=" + + mServiceDataUuid + ", mServiceData=" + Arrays.toString(mServiceData) + + ", mIncludeTxPowerLevel=" + mIncludeTxPowerLevel + "]"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mServiceUuids == null) { + dest.writeInt(0); + } else { + dest.writeInt(mServiceUuids.size()); + dest.writeList(mServiceUuids); + } + + dest.writeInt(mManufacturerId); + if (mManufacturerSpecificData == null) { + dest.writeInt(0); + } else { + dest.writeInt(mManufacturerSpecificData.length); + dest.writeByteArray(mManufacturerSpecificData); + } + + if (mServiceDataUuid == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + dest.writeParcelable(mServiceDataUuid, flags); + if (mServiceData == null) { + dest.writeInt(0); + } else { + dest.writeInt(mServiceData.length); + dest.writeByteArray(mServiceData); + } + } + dest.writeByte((byte) (getIncludeTxPowerLevel() ? 1 : 0)); + } + + public static final Parcelable.Creator<AdvertisementData> CREATOR = + new Creator<AdvertisementData>() { + @Override + public AdvertisementData[] newArray(int size) { + return new AdvertisementData[size]; + } + + @Override + public AdvertisementData createFromParcel(Parcel in) { + Builder builder = new Builder(); + if (in.readInt() > 0) { + List<ParcelUuid> uuids = new ArrayList<ParcelUuid>(); + in.readList(uuids, ParcelUuid.class.getClassLoader()); + builder.setServiceUuids(uuids); + } + int manufacturerId = in.readInt(); + int manufacturerDataLength = in.readInt(); + if (manufacturerDataLength > 0) { + byte[] manufacturerData = new byte[manufacturerDataLength]; + in.readByteArray(manufacturerData); + builder.setManufacturerData(manufacturerId, manufacturerData); + } + if (in.readInt() == 1) { + ParcelUuid serviceDataUuid = in.readParcelable( + ParcelUuid.class.getClassLoader()); + int serviceDataLength = in.readInt(); + if (serviceDataLength > 0) { + byte[] serviceData = new byte[serviceDataLength]; + in.readByteArray(serviceData); + builder.setServiceData(serviceDataUuid, serviceData); + } + } + builder.setIncludeTxPowerLevel(in.readByte() == 1); + return builder.build(); + } + }; + + /** + * Builder for {@link AdvertisementData}. + */ + public static final class Builder { + private static final int MAX_ADVERTISING_DATA_BYTES = 31; + // Each fields need one byte for field length and another byte for field type. + private static final int OVERHEAD_BYTES_PER_FIELD = 2; + // Flags field will be set by system. + private static final int FLAGS_FIELD_BYTES = 3; + + @Nullable + private List<ParcelUuid> mServiceUuids; + private boolean mIncludeTxPowerLevel; + private int mManufacturerId; + @Nullable + private byte[] mManufacturerSpecificData; + @Nullable + private ParcelUuid mServiceDataUuid; + @Nullable + private byte[] mServiceData; + + /** + * Set the service uuids. Note the corresponding bluetooth Gatt services need to be already + * added on the device before start BLE advertising. + * + * @param serviceUuids Service uuids to be advertised, could be 16-bit, 32-bit or 128-bit + * uuids. + * @throws IllegalArgumentException If the {@code serviceUuids} are null. + */ + public Builder setServiceUuids(List<ParcelUuid> serviceUuids) { + if (serviceUuids == null) { + throw new IllegalArgumentException("serivceUuids are null"); + } + mServiceUuids = serviceUuids; + return this; + } + + /** + * Add service data to advertisement. + * + * @param serviceDataUuid A 16 bit uuid of the service data + * @param serviceData Service data - the first two bytes of the service data are the service + * data uuid. + * @throws IllegalArgumentException If the {@code serviceDataUuid} or {@code serviceData} is + * empty. + */ + public Builder setServiceData(ParcelUuid serviceDataUuid, byte[] serviceData) { + if (serviceDataUuid == null || serviceData == null) { + throw new IllegalArgumentException( + "serviceDataUuid or serviceDataUuid is null"); + } + mServiceDataUuid = serviceDataUuid; + mServiceData = serviceData; + return this; + } + + /** + * Set manufacturer id and data. See <a + * href="https://www.bluetooth.org/en-us/specification/assigned-numbers/company-identifiers">assigned + * manufacturer identifies</a> for the existing company identifiers. + * + * @param manufacturerId Manufacturer id assigned by Bluetooth SIG. + * @param manufacturerSpecificData Manufacturer specific data - the first two bytes of the + * manufacturer specific data are the manufacturer id. + * @throws IllegalArgumentException If the {@code manufacturerId} is negative or + * {@code manufacturerSpecificData} is null. + */ + public Builder setManufacturerData(int manufacturerId, byte[] manufacturerSpecificData) { + if (manufacturerId < 0) { + throw new IllegalArgumentException( + "invalid manufacturerId - " + manufacturerId); + } + if (manufacturerSpecificData == null) { + throw new IllegalArgumentException("manufacturerSpecificData is null"); + } + mManufacturerId = manufacturerId; + mManufacturerSpecificData = manufacturerSpecificData; + return this; + } + + /** + * Whether the transmission power level should be included in the advertising packet. + */ + public Builder setIncludeTxPowerLevel(boolean includeTxPowerLevel) { + mIncludeTxPowerLevel = includeTxPowerLevel; + return this; + } + + /** + * Build the {@link AdvertisementData}. + * + * @throws IllegalArgumentException If the data size is larger than 31 bytes. + */ + public AdvertisementData build() { + if (totalBytes() > MAX_ADVERTISING_DATA_BYTES) { + throw new IllegalArgumentException( + "advertisement data size is larger than 31 bytes"); + } + return new AdvertisementData(mServiceUuids, + mServiceDataUuid, + mServiceData, mManufacturerId, mManufacturerSpecificData, + mIncludeTxPowerLevel); + } + + // Compute the size of the advertisement data. + private int totalBytes() { + int size = FLAGS_FIELD_BYTES; // flags field is always set. + if (mServiceUuids != null) { + int num16BitUuids = 0; + int num32BitUuids = 0; + int num128BitUuids = 0; + for (ParcelUuid uuid : mServiceUuids) { + if (BluetoothUuid.is16BitUuid(uuid)) { + ++num16BitUuids; + } else if (BluetoothUuid.is32BitUuid(uuid)) { + ++num32BitUuids; + } else { + ++num128BitUuids; + } + } + // 16 bit service uuids are grouped into one field when doing advertising. + if (num16BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num16BitUuids * BluetoothUuid.UUID_BYTES_16_BIT; + } + // 32 bit service uuids are grouped into one field when doing advertising. + if (num32BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num32BitUuids * BluetoothUuid.UUID_BYTES_32_BIT; + } + // 128 bit service uuids are grouped into one field when doing advertising. + if (num128BitUuids != 0) { + size += OVERHEAD_BYTES_PER_FIELD + + num128BitUuids * BluetoothUuid.UUID_BYTES_128_BIT; + } + } + if (mServiceData != null) { + size += OVERHEAD_BYTES_PER_FIELD + mServiceData.length; + } + if (mManufacturerSpecificData != null) { + size += OVERHEAD_BYTES_PER_FIELD + mManufacturerSpecificData.length; + } + if (mIncludeTxPowerLevel) { + size += OVERHEAD_BYTES_PER_FIELD + 1; // tx power level value is one byte. + } + return size; + } + } +} diff --git a/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java new file mode 100644 index 0000000..ed43407 --- /dev/null +++ b/core/java/android/bluetooth/le/BluetoothLeAdvertiser.java @@ -0,0 +1,368 @@ +/* + * 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.bluetooth.le; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.IBluetoothGatt; +import android.bluetooth.IBluetoothGattCallback; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.util.Log; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * This class provides a way to perform Bluetooth LE advertise operations, such as start and stop + * advertising. An advertiser can broadcast up to 31 bytes of advertisement data represented by + * {@link AdvertisementData}. + * <p> + * To get an instance of {@link BluetoothLeAdvertiser}, call the + * {@link BluetoothAdapter#getBluetoothLeAdvertiser()} method. + * <p> + * Note most of the methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @see AdvertisementData + */ +public final class BluetoothLeAdvertiser { + + private static final String TAG = "BluetoothLeAdvertiser"; + + private final IBluetoothGatt mBluetoothGatt; + private final Handler mHandler; + private final Map<AdvertiseCallback, AdvertiseCallbackWrapper> + mLeAdvertisers = new HashMap<AdvertiseCallback, AdvertiseCallbackWrapper>(); + + /** + * Use BluetoothAdapter.getLeAdvertiser() instead. + * + * @param bluetoothGatt + * @hide + */ + public BluetoothLeAdvertiser(IBluetoothGatt bluetoothGatt) { + mBluetoothGatt = bluetoothGatt; + mHandler = new Handler(Looper.getMainLooper()); + } + + /** + * Start Bluetooth LE Advertising. The {@code advertiseData} would be broadcasted after the + * operation succeeds. Returns immediately, the operation status are delivered through + * {@code callback}. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param settings Settings for Bluetooth LE advertising. + * @param advertiseData Advertisement data to be broadcasted. + * @param callback Callback for advertising status. + */ + public void startAdvertising(AdvertiseSettings settings, + AdvertisementData advertiseData, final AdvertiseCallback callback) { + startAdvertising(settings, advertiseData, null, callback); + } + + /** + * Start Bluetooth LE Advertising. The {@code advertiseData} would be broadcasted after the + * operation succeeds. The {@code scanResponse} would be returned when the scanning device sends + * active scan request. Method returns immediately, the operation status are delivered through + * {@code callback}. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * + * @param settings Settings for Bluetooth LE advertising. + * @param advertiseData Advertisement data to be advertised in advertisement packet. + * @param scanResponse Scan response associated with the advertisement data. + * @param callback Callback for advertising status. + */ + public void startAdvertising(AdvertiseSettings settings, + AdvertisementData advertiseData, AdvertisementData scanResponse, + final AdvertiseCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (mLeAdvertisers.containsKey(callback)) { + postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_ALREADY_STARTED); + return; + } + AdvertiseCallbackWrapper wrapper = new AdvertiseCallbackWrapper(callback, advertiseData, + scanResponse, settings, mBluetoothGatt); + UUID uuid = UUID.randomUUID(); + try { + mBluetoothGatt.registerClient(new ParcelUuid(uuid), wrapper); + if (wrapper.advertiseStarted()) { + mLeAdvertisers.put(callback, wrapper); + } + } catch (RemoteException e) { + Log.e(TAG, "failed to stop advertising", e); + } + } + + /** + * Stop Bluetooth LE advertising. The {@code callback} must be the same one use in + * {@link BluetoothLeAdvertiser#startAdvertising}. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param callback {@link AdvertiseCallback} for delivering stopping advertising status. + */ + public void stopAdvertising(final AdvertiseCallback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + AdvertiseCallbackWrapper wrapper = mLeAdvertisers.get(callback); + if (wrapper == null) { + postCallbackFailure(callback, AdvertiseCallback.ADVERTISE_FAILED_NOT_STARTED); + return; + } + try { + mBluetoothGatt.stopMultiAdvertising(wrapper.mLeHandle); + if (wrapper.advertiseStopped()) { + mLeAdvertisers.remove(callback); + } + } catch (RemoteException e) { + Log.e(TAG, "failed to stop advertising", e); + } + } + + /** + * Bluetooth GATT interface callbacks for advertising. + */ + private static class AdvertiseCallbackWrapper extends IBluetoothGattCallback.Stub { + private static final int LE_CALLBACK_TIMEOUT_MILLIS = 2000; + private final AdvertiseCallback mAdvertiseCallback; + private final AdvertisementData mAdvertisement; + private final AdvertisementData mScanResponse; + private final AdvertiseSettings mSettings; + private final IBluetoothGatt mBluetoothGatt; + + // mLeHandle 0: not registered + // -1: scan stopped + // >0: registered and scan started + private int mLeHandle; + private boolean isAdvertising = false; + + public AdvertiseCallbackWrapper(AdvertiseCallback advertiseCallback, + AdvertisementData advertiseData, AdvertisementData scanResponse, + AdvertiseSettings settings, + IBluetoothGatt bluetoothGatt) { + mAdvertiseCallback = advertiseCallback; + mAdvertisement = advertiseData; + mScanResponse = scanResponse; + mSettings = settings; + mBluetoothGatt = bluetoothGatt; + mLeHandle = 0; + } + + public boolean advertiseStarted() { + boolean started = false; + synchronized (this) { + if (mLeHandle == -1) { + return false; + } + try { + wait(LE_CALLBACK_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + Log.e(TAG, "Callback reg wait interrupted: ", e); + } + started = (mLeHandle > 0 && isAdvertising); + } + return started; + } + + public boolean advertiseStopped() { + synchronized (this) { + try { + wait(LE_CALLBACK_TIMEOUT_MILLIS); + } catch (InterruptedException e) { + Log.e(TAG, "Callback reg wait interrupted: " + e); + } + return !isAdvertising; + } + } + + /** + * Application interface registered - app is ready to go + */ + @Override + public void onClientRegistered(int status, int clientIf) { + Log.d(TAG, "onClientRegistered() - status=" + status + " clientIf=" + clientIf); + synchronized (this) { + if (status == BluetoothGatt.GATT_SUCCESS) { + mLeHandle = clientIf; + try { + mBluetoothGatt.startMultiAdvertising(mLeHandle, mAdvertisement, + mScanResponse, mSettings); + } catch (RemoteException e) { + Log.e(TAG, "fail to start le advertise: " + e); + mLeHandle = -1; + notifyAll(); + } catch (Exception e) { + Log.e(TAG, "fail to start advertise: " + e.getStackTrace()); + } + } else { + // registration failed + mLeHandle = -1; + notifyAll(); + } + } + } + + @Override + public void onClientConnectionState(int status, int clientIf, + boolean connected, String address) { + // no op + } + + @Override + public void onScanResult(String address, int rssi, byte[] advData) { + // no op + } + + @Override + public void onGetService(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid) { + // no op + } + + @Override + public void onGetIncludedService(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int inclSrvcType, int inclSrvcInstId, + ParcelUuid inclSrvcUuid) { + // no op + } + + @Override + public void onGetCharacteristic(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int charProps) { + // no op + } + + @Override + public void onGetDescriptor(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descUuid) { + // no op + } + + @Override + public void onSearchComplete(String address, int status) { + // no op + } + + @Override + public void onCharacteristicRead(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, byte[] value) { + // no op + } + + @Override + public void onCharacteristicWrite(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid) { + // no op + } + + @Override + public void onNotify(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + byte[] value) { + // no op + } + + @Override + public void onDescriptorRead(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descrUuid, byte[] value) { + // no op + } + + @Override + public void onDescriptorWrite(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descrUuid) { + // no op + } + + @Override + public void onExecuteWrite(String address, int status) { + // no op + } + + @Override + public void onReadRemoteRssi(String address, int rssi, int status) { + // no op + } + + @Override + public void onAdvertiseStateChange(int advertiseState, int status) { + // no op + } + + @Override + public void onMultiAdvertiseCallback(int status) { + synchronized (this) { + if (status == 0) { + isAdvertising = !isAdvertising; + if (!isAdvertising) { + try { + mBluetoothGatt.unregisterClient(mLeHandle); + mLeHandle = -1; + } catch (RemoteException e) { + Log.e(TAG, "remote exception when unregistering", e); + } + } + mAdvertiseCallback.onSuccess(null); + } else { + mAdvertiseCallback.onFailure(status); + } + notifyAll(); + } + + } + + /** + * Callback reporting LE ATT MTU. + * + * @hide + */ + @Override + public void onConfigureMTU(String address, int mtu, int status) { + // no op + } + } + + private void postCallbackFailure(final AdvertiseCallback callback, final int error) { + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onFailure(error); + } + }); + } +} diff --git a/core/java/android/bluetooth/le/BluetoothLeScanner.java b/core/java/android/bluetooth/le/BluetoothLeScanner.java new file mode 100644 index 0000000..4c6346c --- /dev/null +++ b/core/java/android/bluetooth/le/BluetoothLeScanner.java @@ -0,0 +1,371 @@ +/* + * 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.bluetooth.le; + +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import android.bluetooth.IBluetoothGatt; +import android.bluetooth.IBluetoothGattCallback; +import android.os.Handler; +import android.os.Looper; +import android.os.ParcelUuid; +import android.os.RemoteException; +import android.os.SystemClock; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + * This class provides methods to perform scan related operations for Bluetooth LE devices. An + * application can scan for a particular type of BLE devices using {@link ScanFilter}. It can also + * request different types of callbacks for delivering the result. + * <p> + * Use {@link BluetoothAdapter#getBluetoothLeScanner()} to get an instance of + * {@link BluetoothLeScanner}. + * <p> + * Note most of the scan methods here require {@link android.Manifest.permission#BLUETOOTH_ADMIN} + * permission. + * + * @see ScanFilter + */ +public final class BluetoothLeScanner { + + private static final String TAG = "BluetoothLeScanner"; + private static final boolean DBG = true; + + private final IBluetoothGatt mBluetoothGatt; + private final Handler mHandler; + private final Map<ScanCallback, BleScanCallbackWrapper> mLeScanClients; + + /** + * @hide + */ + public BluetoothLeScanner(IBluetoothGatt bluetoothGatt) { + mBluetoothGatt = bluetoothGatt; + mHandler = new Handler(Looper.getMainLooper()); + mLeScanClients = new HashMap<ScanCallback, BleScanCallbackWrapper>(); + } + + /** + * Start Bluetooth LE scan. The scan results will be delivered through {@code callback}. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param filters {@link ScanFilter}s for finding exact BLE devices. + * @param settings Settings for ble scan. + * @param callback Callback when scan results are delivered. + * @throws IllegalArgumentException If {@code settings} or {@code callback} is null. + */ + public void startScan(List<ScanFilter> filters, ScanSettings settings, + final ScanCallback callback) { + if (settings == null || callback == null) { + throw new IllegalArgumentException("settings or callback is null"); + } + synchronized (mLeScanClients) { + if (mLeScanClients.containsKey(callback)) { + postCallbackError(callback, ScanCallback.SCAN_FAILED_ALREADY_STARTED); + return; + } + BleScanCallbackWrapper wrapper = new BleScanCallbackWrapper(mBluetoothGatt, filters, + settings, callback); + try { + UUID uuid = UUID.randomUUID(); + mBluetoothGatt.registerClient(new ParcelUuid(uuid), wrapper); + if (wrapper.scanStarted()) { + mLeScanClients.put(callback, wrapper); + } else { + postCallbackError(callback, + ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED); + return; + } + } catch (RemoteException e) { + Log.e(TAG, "GATT service exception when starting scan", e); + postCallbackError(callback, ScanCallback.SCAN_FAILED_GATT_SERVICE_FAILURE); + } + } + } + + /** + * Stops an ongoing Bluetooth LE scan. + * <p> + * Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN} permission. + * + * @param callback + */ + public void stopScan(ScanCallback callback) { + synchronized (mLeScanClients) { + BleScanCallbackWrapper wrapper = mLeScanClients.remove(callback); + if (wrapper == null) { + return; + } + wrapper.stopLeScan(); + } + } + + /** + * Returns available storage size for batch scan results. It's recommended not to use batch scan + * if available storage size is small (less than 1k bytes, for instance). + * + * @hide TODO: unhide when batching is supported in stack. + */ + public int getAvailableBatchStorageSizeBytes() { + throw new UnsupportedOperationException("not impelemented"); + } + + /** + * Poll scan results from bluetooth controller. This will return Bluetooth LE scan results + * batched on bluetooth controller. + * + * @param callback Callback of the Bluetooth LE Scan, it has to be the same instance as the one + * used to start scan. + * @param flush Whether to flush the batch scan buffer. Note the other batch scan clients will + * get batch scan callback if the batch scan buffer is flushed. + * @return Batch Scan results. + * @hide TODO: unhide when batching is supported in stack. + */ + public List<ScanResult> getBatchScanResults(ScanCallback callback, boolean flush) { + throw new UnsupportedOperationException("not impelemented"); + } + + /** + * Bluetooth GATT interface callbacks + */ + private static class BleScanCallbackWrapper extends IBluetoothGattCallback.Stub { + private static final int REGISTRATION_CALLBACK_TIMEOUT_SECONDS = 5; + + private final ScanCallback mScanCallback; + private final List<ScanFilter> mFilters; + private ScanSettings mSettings; + private IBluetoothGatt mBluetoothGatt; + + // mLeHandle 0: not registered + // -1: scan stopped + // > 0: registered and scan started + private int mLeHandle; + + public BleScanCallbackWrapper(IBluetoothGatt bluetoothGatt, + List<ScanFilter> filters, ScanSettings settings, + ScanCallback scanCallback) { + mBluetoothGatt = bluetoothGatt; + mFilters = filters; + mSettings = settings; + mScanCallback = scanCallback; + mLeHandle = 0; + } + + public boolean scanStarted() { + synchronized (this) { + if (mLeHandle == -1) { + return false; + } + try { + wait(REGISTRATION_CALLBACK_TIMEOUT_SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, "Callback reg wait interrupted: " + e); + } + } + return mLeHandle > 0; + } + + public void stopLeScan() { + synchronized (this) { + if (mLeHandle <= 0) { + Log.e(TAG, "Error state, mLeHandle: " + mLeHandle); + return; + } + try { + mBluetoothGatt.stopScan(mLeHandle, false); + mBluetoothGatt.unregisterClient(mLeHandle); + } catch (RemoteException e) { + Log.e(TAG, "Failed to stop scan and unregister" + e); + } + mLeHandle = -1; + notifyAll(); + } + } + + /** + * Application interface registered - app is ready to go + */ + @Override + public void onClientRegistered(int status, int clientIf) { + Log.d(TAG, "onClientRegistered() - status=" + status + + " clientIf=" + clientIf); + + synchronized (this) { + if (mLeHandle == -1) { + if (DBG) + Log.d(TAG, "onClientRegistered LE scan canceled"); + } + + if (status == BluetoothGatt.GATT_SUCCESS) { + mLeHandle = clientIf; + try { + mBluetoothGatt.startScanWithFilters(mLeHandle, false, mSettings, mFilters); + } catch (RemoteException e) { + Log.e(TAG, "fail to start le scan: " + e); + mLeHandle = -1; + } + } else { + // registration failed + mLeHandle = -1; + } + notifyAll(); + } + } + + @Override + public void onClientConnectionState(int status, int clientIf, + boolean connected, String address) { + // no op + } + + /** + * Callback reporting an LE scan result. + * + * @hide + */ + @Override + public void onScanResult(String address, int rssi, byte[] advData) { + if (DBG) + Log.d(TAG, "onScanResult() - Device=" + address + " RSSI=" + rssi); + + // Check null in case the scan has been stopped + synchronized (this) { + if (mLeHandle <= 0) + return; + } + BluetoothDevice device = BluetoothAdapter.getDefaultAdapter().getRemoteDevice( + address); + long scanNanos = SystemClock.elapsedRealtimeNanos(); + ScanResult result = new ScanResult(device, advData, rssi, + scanNanos); + mScanCallback.onAdvertisementUpdate(result); + } + + @Override + public void onGetService(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid) { + // no op + } + + @Override + public void onGetIncludedService(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int inclSrvcType, int inclSrvcInstId, + ParcelUuid inclSrvcUuid) { + // no op + } + + @Override + public void onGetCharacteristic(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int charProps) { + // no op + } + + @Override + public void onGetDescriptor(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descUuid) { + // no op + } + + @Override + public void onSearchComplete(String address, int status) { + // no op + } + + @Override + public void onCharacteristicRead(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, byte[] value) { + // no op + } + + @Override + public void onCharacteristicWrite(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid) { + // no op + } + + @Override + public void onNotify(String address, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + byte[] value) { + // no op + } + + @Override + public void onDescriptorRead(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descrUuid, byte[] value) { + // no op + } + + @Override + public void onDescriptorWrite(String address, int status, int srvcType, + int srvcInstId, ParcelUuid srvcUuid, + int charInstId, ParcelUuid charUuid, + int descInstId, ParcelUuid descrUuid) { + // no op + } + + @Override + public void onExecuteWrite(String address, int status) { + // no op + } + + @Override + public void onReadRemoteRssi(String address, int rssi, int status) { + // no op + } + + @Override + public void onAdvertiseStateChange(int advertiseState, int status) { + // no op + } + + @Override + public void onMultiAdvertiseCallback(int status) { + // no op + } + + @Override + public void onConfigureMTU(String address, int mtu, int status) { + // no op + } + } + + private void postCallbackError(final ScanCallback callback, final int errorCode) { + mHandler.post(new Runnable() { + @Override + public void run() { + callback.onScanFailed(errorCode); + } + }); + } +} diff --git a/core/java/android/bluetooth/le/ScanCallback.java b/core/java/android/bluetooth/le/ScanCallback.java new file mode 100644 index 0000000..50ebf50 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanCallback.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.bluetooth.le; + +import java.util.List; + +/** + * Callback of Bluetooth LE scans. The results of the scans will be delivered through the callbacks. + */ +public abstract class ScanCallback { + + /** + * Fails to start scan as BLE scan with the same settings is already started by the app. + */ + public static final int SCAN_FAILED_ALREADY_STARTED = 1; + /** + * Fails to start scan as app cannot be registered. + */ + public static final int SCAN_FAILED_APPLICATION_REGISTRATION_FAILED = 2; + /** + * Fails to start scan due to gatt service failure. + */ + public static final int SCAN_FAILED_GATT_SERVICE_FAILURE = 3; + /** + * Fails to start scan due to controller failure. + */ + public static final int SCAN_FAILED_CONTROLLER_FAILURE = 4; + + /** + * Callback when a BLE advertisement is found. + * + * @param result A Bluetooth LE scan result. + */ + public abstract void onAdvertisementUpdate(ScanResult result); + + /** + * Callback when the BLE advertisement is found for the first time. + * + * @param result The Bluetooth LE scan result when the onFound event is triggered. + * @hide + */ + public abstract void onAdvertisementFound(ScanResult result); + + /** + * Callback when the BLE advertisement was lost. Note a device has to be "found" before it's + * lost. + * + * @param result The Bluetooth scan result that was last found. + * @hide + */ + public abstract void onAdvertisementLost(ScanResult result); + + /** + * Callback when batch results are delivered. + * + * @param results List of scan results that are previously scanned. + * @hide + */ + public abstract void onBatchScanResults(List<ScanResult> results); + + /** + * Callback when scan failed. + */ + public abstract void onScanFailed(int errorCode); +} diff --git a/core/java/android/bluetooth/le/ScanFilter.aidl b/core/java/android/bluetooth/le/ScanFilter.aidl new file mode 100644 index 0000000..4cecfe6 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanFilter.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.bluetooth.le; + +parcelable ScanFilter; diff --git a/core/java/android/bluetooth/le/ScanFilter.java b/core/java/android/bluetooth/le/ScanFilter.java new file mode 100644 index 0000000..c2e316b --- /dev/null +++ b/core/java/android/bluetooth/le/ScanFilter.java @@ -0,0 +1,588 @@ +/* + * 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.bluetooth.le; + +import android.annotation.Nullable; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * {@link ScanFilter} abstracts different scan filters across Bluetooth Advertisement packet fields. + * <p> + * Current filtering on the following fields are supported: + * <li>Service UUIDs which identify the bluetooth gatt services running on the device. + * <li>Name of remote Bluetooth LE device. + * <li>Mac address of the remote device. + * <li>Rssi which indicates the received power level. + * <li>Service data which is the data associated with a service. + * <li>Manufacturer specific data which is the data associated with a particular manufacturer. + * + * @see ScanRecord + * @see BluetoothLeScanner + */ +public final class ScanFilter implements Parcelable { + + @Nullable + private final String mLocalName; + + @Nullable + private final String mMacAddress; + + @Nullable + private final ParcelUuid mServiceUuid; + @Nullable + private final ParcelUuid mServiceUuidMask; + + @Nullable + private final byte[] mServiceData; + @Nullable + private final byte[] mServiceDataMask; + + private final int mManufacturerId; + @Nullable + private final byte[] mManufacturerData; + @Nullable + private final byte[] mManufacturerDataMask; + + private final int mMinRssi; + private final int mMaxRssi; + + private ScanFilter(String name, String macAddress, ParcelUuid uuid, + ParcelUuid uuidMask, byte[] serviceData, byte[] serviceDataMask, + int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask, + int minRssi, int maxRssi) { + mLocalName = name; + mServiceUuid = uuid; + mServiceUuidMask = uuidMask; + mMacAddress = macAddress; + mServiceData = serviceData; + mServiceDataMask = serviceDataMask; + mManufacturerId = manufacturerId; + mManufacturerData = manufacturerData; + mManufacturerDataMask = manufacturerDataMask; + mMinRssi = minRssi; + mMaxRssi = maxRssi; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mLocalName == null ? 0 : 1); + if (mLocalName != null) { + dest.writeString(mLocalName); + } + dest.writeInt(mMacAddress == null ? 0 : 1); + if (mMacAddress != null) { + dest.writeString(mMacAddress); + } + dest.writeInt(mServiceUuid == null ? 0 : 1); + if (mServiceUuid != null) { + dest.writeParcelable(mServiceUuid, flags); + dest.writeInt(mServiceUuidMask == null ? 0 : 1); + if (mServiceUuidMask != null) { + dest.writeParcelable(mServiceUuidMask, flags); + } + } + dest.writeInt(mServiceData == null ? 0 : mServiceData.length); + if (mServiceData != null) { + dest.writeByteArray(mServiceData); + dest.writeInt(mServiceDataMask == null ? 0 : mServiceDataMask.length); + if (mServiceDataMask != null) { + dest.writeByteArray(mServiceDataMask); + } + } + dest.writeInt(mManufacturerId); + dest.writeInt(mManufacturerData == null ? 0 : mManufacturerData.length); + if (mManufacturerData != null) { + dest.writeByteArray(mManufacturerData); + dest.writeInt(mManufacturerDataMask == null ? 0 : mManufacturerDataMask.length); + if (mManufacturerDataMask != null) { + dest.writeByteArray(mManufacturerDataMask); + } + } + dest.writeInt(mMinRssi); + dest.writeInt(mMaxRssi); + } + + /** + * A {@link android.os.Parcelable.Creator} to create {@link ScanFilter} form parcel. + */ + public static final Creator<ScanFilter> + CREATOR = new Creator<ScanFilter>() { + + @Override + public ScanFilter[] newArray(int size) { + return new ScanFilter[size]; + } + + @Override + public ScanFilter createFromParcel(Parcel in) { + Builder builder = new Builder(); + if (in.readInt() == 1) { + builder.setName(in.readString()); + } + if (in.readInt() == 1) { + builder.setMacAddress(in.readString()); + } + if (in.readInt() == 1) { + ParcelUuid uuid = in.readParcelable(ParcelUuid.class.getClassLoader()); + builder.setServiceUuid(uuid); + if (in.readInt() == 1) { + ParcelUuid uuidMask = in.readParcelable( + ParcelUuid.class.getClassLoader()); + builder.setServiceUuid(uuid, uuidMask); + } + } + + int serviceDataLength = in.readInt(); + if (serviceDataLength > 0) { + byte[] serviceData = new byte[serviceDataLength]; + in.readByteArray(serviceData); + builder.setServiceData(serviceData); + int serviceDataMaskLength = in.readInt(); + if (serviceDataMaskLength > 0) { + byte[] serviceDataMask = new byte[serviceDataMaskLength]; + in.readByteArray(serviceDataMask); + builder.setServiceData(serviceData, serviceDataMask); + } + } + + int manufacturerId = in.readInt(); + int manufacturerDataLength = in.readInt(); + if (manufacturerDataLength > 0) { + byte[] manufacturerData = new byte[manufacturerDataLength]; + in.readByteArray(manufacturerData); + builder.setManufacturerData(manufacturerId, manufacturerData); + int manufacturerDataMaskLength = in.readInt(); + if (manufacturerDataMaskLength > 0) { + byte[] manufacturerDataMask = new byte[manufacturerDataMaskLength]; + in.readByteArray(manufacturerDataMask); + builder.setManufacturerData(manufacturerId, manufacturerData, + manufacturerDataMask); + } + } + + int minRssi = in.readInt(); + int maxRssi = in.readInt(); + builder.setRssiRange(minRssi, maxRssi); + return builder.build(); + } + }; + + /** + * Returns the filter set the local name field of Bluetooth advertisement data. + */ + @Nullable + public String getLocalName() { + return mLocalName; + } + + /** + * Returns the filter set on the service uuid. + */ + @Nullable + public ParcelUuid getServiceUuid() { + return mServiceUuid; + } + + @Nullable + public ParcelUuid getServiceUuidMask() { + return mServiceUuidMask; + } + + @Nullable + public String getDeviceAddress() { + return mMacAddress; + } + + @Nullable + public byte[] getServiceData() { + return mServiceData; + } + + @Nullable + public byte[] getServiceDataMask() { + return mServiceDataMask; + } + + /** + * Returns the manufacturer id. -1 if the manufacturer filter is not set. + */ + public int getManufacturerId() { + return mManufacturerId; + } + + @Nullable + public byte[] getManufacturerData() { + return mManufacturerData; + } + + @Nullable + public byte[] getManufacturerDataMask() { + return mManufacturerDataMask; + } + + /** + * Returns minimum value of rssi for the scan filter. {@link Integer#MIN_VALUE} if not set. + */ + public int getMinRssi() { + return mMinRssi; + } + + /** + * Returns maximum value of the rssi for the scan filter. {@link Integer#MAX_VALUE} if not set. + */ + public int getMaxRssi() { + return mMaxRssi; + } + + /** + * Check if the scan filter matches a {@code scanResult}. A scan result is considered as a match + * if it matches all the field filters. + */ + public boolean matches(ScanResult scanResult) { + if (scanResult == null) { + return false; + } + BluetoothDevice device = scanResult.getDevice(); + // Device match. + if (mMacAddress != null && (device == null || !mMacAddress.equals(device.getAddress()))) { + return false; + } + + int rssi = scanResult.getRssi(); + if (rssi < mMinRssi || rssi > mMaxRssi) { + return false; + } + + byte[] scanRecordBytes = scanResult.getScanRecord(); + ScanRecord scanRecord = ScanRecord.parseFromBytes(scanRecordBytes); + + // Scan record is null but there exist filters on it. + if (scanRecord == null + && (mLocalName != null || mServiceUuid != null || mManufacturerData != null + || mServiceData != null)) { + return false; + } + + // Local name match. + if (mLocalName != null && !mLocalName.equals(scanRecord.getLocalName())) { + return false; + } + + // UUID match. + if (mServiceUuid != null && !matchesServiceUuids(mServiceUuid, mServiceUuidMask, + scanRecord.getServiceUuids())) { + return false; + } + + // Service data match + if (mServiceData != null && + !matchesPartialData(mServiceData, mServiceDataMask, scanRecord.getServiceData())) { + return false; + } + + // Manufacturer data match. + if (mManufacturerData != null && !matchesPartialData(mManufacturerData, + mManufacturerDataMask, scanRecord.getManufacturerSpecificData())) { + return false; + } + // All filters match. + return true; + } + + // Check if the uuid pattern is contained in a list of parcel uuids. + private boolean matchesServiceUuids(ParcelUuid uuid, ParcelUuid parcelUuidMask, + List<ParcelUuid> uuids) { + if (uuid == null) { + return true; + } + if (uuids == null) { + return false; + } + + for (ParcelUuid parcelUuid : uuids) { + UUID uuidMask = parcelUuidMask == null ? null : parcelUuidMask.getUuid(); + if (matchesServiceUuid(uuid.getUuid(), uuidMask, parcelUuid.getUuid())) { + return true; + } + } + return false; + } + + // Check if the uuid pattern matches the particular service uuid. + private boolean matchesServiceUuid(UUID uuid, UUID mask, UUID data) { + if (mask == null) { + return uuid.equals(data); + } + if ((uuid.getLeastSignificantBits() & mask.getLeastSignificantBits()) != + (data.getLeastSignificantBits() & mask.getLeastSignificantBits())) { + return false; + } + return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits()) == + (data.getMostSignificantBits() & mask.getMostSignificantBits())); + } + + // Check whether the data pattern matches the parsed data. + private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) { + if (dataMask == null) { + return Arrays.equals(data, parsedData); + } + if (parsedData == null) { + return false; + } + for (int i = 0; i < data.length; ++i) { + if ((dataMask[i] & parsedData[i]) != (dataMask[i] & data[i])) { + return false; + } + } + return true; + } + + @Override + public String toString() { + return "BluetoothLeScanFilter [mLocalName=" + mLocalName + ", mMacAddress=" + mMacAddress + + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask + ", mServiceData=" + + Arrays.toString(mServiceData) + ", mServiceDataMask=" + + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId + + ", mManufacturerData=" + Arrays.toString(mManufacturerData) + + ", mManufacturerDataMask=" + Arrays.toString(mManufacturerDataMask) + + ", mMinRssi=" + mMinRssi + ", mMaxRssi=" + mMaxRssi + "]"; + } + + @Override + public int hashCode() { + return Objects.hash(mLocalName, mMacAddress, mManufacturerId, mManufacturerData, + mManufacturerDataMask, mMaxRssi, mMinRssi, mServiceData, mServiceDataMask, + mServiceUuid, mServiceUuidMask); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ScanFilter other = (ScanFilter) obj; + return Objects.equals(mLocalName, other.mLocalName) && + Objects.equals(mMacAddress, other.mMacAddress) && + mManufacturerId == other.mManufacturerId && + Objects.deepEquals(mManufacturerData, other.mManufacturerData) && + Objects.deepEquals(mManufacturerDataMask, other.mManufacturerDataMask) && + mMinRssi == other.mMinRssi && mMaxRssi == other.mMaxRssi && + Objects.deepEquals(mServiceData, other.mServiceData) && + Objects.deepEquals(mServiceDataMask, other.mServiceDataMask) && + Objects.equals(mServiceUuid, other.mServiceUuid) && + Objects.equals(mServiceUuidMask, other.mServiceUuidMask); + } + + /** + * Builder class for {@link ScanFilter}. + */ + public static final class Builder { + + private String mLocalName; + private String mMacAddress; + + private ParcelUuid mServiceUuid; + private ParcelUuid mUuidMask; + + private byte[] mServiceData; + private byte[] mServiceDataMask; + + private int mManufacturerId = -1; + private byte[] mManufacturerData; + private byte[] mManufacturerDataMask; + + private int mMinRssi = Integer.MIN_VALUE; + private int mMaxRssi = Integer.MAX_VALUE; + + /** + * Set filter on local name. + */ + public Builder setName(String localName) { + mLocalName = localName; + return this; + } + + /** + * Set filter on device mac address. + * + * @param macAddress The device mac address for the filter. It needs to be in the format of + * "01:02:03:AB:CD:EF". The mac address can be validated using + * {@link BluetoothAdapter#checkBluetoothAddress}. + * @throws IllegalArgumentException If the {@code macAddress} is invalid. + */ + public Builder setMacAddress(String macAddress) { + if (macAddress != null && !BluetoothAdapter.checkBluetoothAddress(macAddress)) { + throw new IllegalArgumentException("invalid mac address " + macAddress); + } + mMacAddress = macAddress; + return this; + } + + /** + * Set filter on service uuid. + */ + public Builder setServiceUuid(ParcelUuid serviceUuid) { + mServiceUuid = serviceUuid; + mUuidMask = null; // clear uuid mask + return this; + } + + /** + * Set filter on partial service uuid. The {@code uuidMask} is the bit mask for the + * {@code serviceUuid}. Set any bit in the mask to 1 to indicate a match is needed for the + * bit in {@code serviceUuid}, and 0 to ignore that bit. + * + * @throws IllegalArgumentException If {@code serviceUuid} is {@code null} but + * {@code uuidMask} is not {@code null}. + */ + public Builder setServiceUuid(ParcelUuid serviceUuid, ParcelUuid uuidMask) { + if (mUuidMask != null && mServiceUuid == null) { + throw new IllegalArgumentException("uuid is null while uuidMask is not null!"); + } + mServiceUuid = serviceUuid; + mUuidMask = uuidMask; + return this; + } + + /** + * Set filtering on service data. + */ + public Builder setServiceData(byte[] serviceData) { + mServiceData = serviceData; + mServiceDataMask = null; // clear service data mask + return this; + } + + /** + * Set partial filter on service data. For any bit in the mask, set it to 1 if it needs to + * match the one in service data, otherwise set it to 0 to ignore that bit. + * <p> + * The {@code serviceDataMask} must have the same length of the {@code serviceData}. + * + * @throws IllegalArgumentException If {@code serviceDataMask} is {@code null} while + * {@code serviceData} is not or {@code serviceDataMask} and {@code serviceData} + * has different length. + */ + public Builder setServiceData(byte[] serviceData, byte[] serviceDataMask) { + if (mServiceDataMask != null) { + if (mServiceData == null) { + throw new IllegalArgumentException( + "serviceData is null while serviceDataMask is not null"); + } + // Since the mServiceDataMask is a bit mask for mServiceData, the lengths of the two + // byte array need to be the same. + if (mServiceData.length != mServiceDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for service data and service data mask"); + } + } + mServiceData = serviceData; + mServiceDataMask = serviceDataMask; + return this; + } + + /** + * Set filter on on manufacturerData. A negative manufacturerId is considered as invalid id. + * <p> + * Note the first two bytes of the {@code manufacturerData} is the manufacturerId. + * + * @throws IllegalArgumentException If the {@code manufacturerId} is invalid. + */ + public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData) { + if (manufacturerData != null && manufacturerId < 0) { + throw new IllegalArgumentException("invalid manufacture id"); + } + mManufacturerId = manufacturerId; + mManufacturerData = manufacturerData; + mManufacturerDataMask = null; // clear manufacturer data mask + return this; + } + + /** + * Set filter on partial manufacture data. For any bit in the mask, set it the 1 if it + * needs to match the one in manufacturer data, otherwise set it to 0. + * <p> + * The {@code manufacturerDataMask} must have the same length of {@code manufacturerData}. + * + * @throws IllegalArgumentException If the {@code manufacturerId} is invalid, or + * {@code manufacturerData} is null while {@code manufacturerDataMask} is not, + * or {@code manufacturerData} and {@code manufacturerDataMask} have different + * length. + */ + public Builder setManufacturerData(int manufacturerId, byte[] manufacturerData, + byte[] manufacturerDataMask) { + if (manufacturerData != null && manufacturerId < 0) { + throw new IllegalArgumentException("invalid manufacture id"); + } + if (mManufacturerDataMask != null) { + if (mManufacturerData == null) { + throw new IllegalArgumentException( + "manufacturerData is null while manufacturerDataMask is not null"); + } + // Since the mManufacturerDataMask is a bit mask for mManufacturerData, the lengths + // of the two byte array need to be the same. + if (mManufacturerData.length != mManufacturerDataMask.length) { + throw new IllegalArgumentException( + "size mismatch for manufacturerData and manufacturerDataMask"); + } + } + mManufacturerId = manufacturerId; + mManufacturerData = manufacturerData; + mManufacturerDataMask = manufacturerDataMask; + return this; + } + + /** + * Set the desired rssi range for the filter. A scan result with rssi in the range of + * [minRssi, maxRssi] will be consider as a match. + */ + public Builder setRssiRange(int minRssi, int maxRssi) { + mMinRssi = minRssi; + mMaxRssi = maxRssi; + return this; + } + + /** + * Build {@link ScanFilter}. + * + * @throws IllegalArgumentException If the filter cannot be built. + */ + public ScanFilter build() { + return new ScanFilter(mLocalName, mMacAddress, + mServiceUuid, mUuidMask, + mServiceData, mServiceDataMask, + mManufacturerId, mManufacturerData, mManufacturerDataMask, mMinRssi, mMaxRssi); + } + } +} diff --git a/core/java/android/bluetooth/le/ScanRecord.java b/core/java/android/bluetooth/le/ScanRecord.java new file mode 100644 index 0000000..bd7304b --- /dev/null +++ b/core/java/android/bluetooth/le/ScanRecord.java @@ -0,0 +1,278 @@ +/* + * 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.bluetooth.le; + +import android.annotation.Nullable; +import android.bluetooth.BluetoothUuid; +import android.os.ParcelUuid; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a scan record from Bluetooth LE scan. + */ +public final class ScanRecord { + + private static final String TAG = "ScanRecord"; + + // The following data type values are assigned by Bluetooth SIG. + // For more details refer to Bluetooth 4.1 specification, Volume 3, Part C, Section 18. + private static final int DATA_TYPE_FLAGS = 0x01; + private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL = 0x02; + private static final int DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE = 0x03; + private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL = 0x04; + private static final int DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE = 0x05; + private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL = 0x06; + private static final int DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE = 0x07; + private static final int DATA_TYPE_LOCAL_NAME_SHORT = 0x08; + private static final int DATA_TYPE_LOCAL_NAME_COMPLETE = 0x09; + private static final int DATA_TYPE_TX_POWER_LEVEL = 0x0A; + private static final int DATA_TYPE_SERVICE_DATA = 0x16; + private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF; + + // Flags of the advertising data. + private final int mAdvertiseFlags; + + @Nullable + private final List<ParcelUuid> mServiceUuids; + + private final int mManufacturerId; + @Nullable + private final byte[] mManufacturerSpecificData; + + @Nullable + private final ParcelUuid mServiceDataUuid; + @Nullable + private final byte[] mServiceData; + + // Transmission power level(in dB). + private final int mTxPowerLevel; + + // Local name of the Bluetooth LE device. + private final String mLocalName; + + /** + * Returns the advertising flags indicating the discoverable mode and capability of the device. + * Returns -1 if the flag field is not set. + */ + public int getAdvertiseFlags() { + return mAdvertiseFlags; + } + + /** + * Returns a list of service uuids within the advertisement that are used to identify the + * bluetooth gatt services. + */ + public List<ParcelUuid> getServiceUuids() { + return mServiceUuids; + } + + /** + * Returns the manufacturer identifier, which is a non-negative number assigned by Bluetooth + * SIG. + */ + public int getManufacturerId() { + return mManufacturerId; + } + + /** + * Returns the manufacturer specific data which is the content of manufacturer specific data + * field. The first 2 bytes of the data contain the company id. + */ + public byte[] getManufacturerSpecificData() { + return mManufacturerSpecificData; + } + + /** + * Returns a 16 bit uuid of the service that the service data is associated with. + */ + public ParcelUuid getServiceDataUuid() { + return mServiceDataUuid; + } + + /** + * Returns service data. The first two bytes should be a 16 bit service uuid associated with the + * service data. + */ + public byte[] getServiceData() { + return mServiceData; + } + + /** + * Returns the transmission power level of the packet in dBm. Returns {@link Integer#MIN_VALUE} + * if the field is not set. This value can be used to calculate the path loss of a received + * packet using the following equation: + * <p> + * <code>pathloss = txPowerLevel - rssi</code> + */ + public int getTxPowerLevel() { + return mTxPowerLevel; + } + + /** + * Returns the local name of the BLE device. The is a UTF-8 encoded string. + */ + @Nullable + public String getLocalName() { + return mLocalName; + } + + private ScanRecord(List<ParcelUuid> serviceUuids, + ParcelUuid serviceDataUuid, byte[] serviceData, + int manufacturerId, + byte[] manufacturerSpecificData, int advertiseFlags, int txPowerLevel, + String localName) { + mServiceUuids = serviceUuids; + mManufacturerId = manufacturerId; + mManufacturerSpecificData = manufacturerSpecificData; + mServiceDataUuid = serviceDataUuid; + mServiceData = serviceData; + mLocalName = localName; + mAdvertiseFlags = advertiseFlags; + mTxPowerLevel = txPowerLevel; + } + + @Override + public String toString() { + return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids + + ", mManufacturerId=" + mManufacturerId + ", mManufacturerSpecificData=" + + Arrays.toString(mManufacturerSpecificData) + ", mServiceDataUuid=" + + mServiceDataUuid + ", mServiceData=" + Arrays.toString(mServiceData) + + ", mTxPowerLevel=" + mTxPowerLevel + ", mLocalName=" + mLocalName + "]"; + } + + /** + * Parse scan record bytes to {@link ScanRecord}. + * <p> + * The format is defined in Bluetooth 4.1 specification, Volume 3, Part C, Section 11 and 18. + * <p> + * All numerical multi-byte entities and values shall use little-endian <strong>byte</strong> + * order. + * + * @param scanRecord The scan record of Bluetooth LE advertisement and/or scan response. + */ + public static ScanRecord parseFromBytes(byte[] scanRecord) { + if (scanRecord == null) { + return null; + } + + int currentPos = 0; + int advertiseFlag = -1; + List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>(); + String localName = null; + int txPowerLevel = Integer.MIN_VALUE; + ParcelUuid serviceDataUuid = null; + byte[] serviceData = null; + int manufacturerId = -1; + byte[] manufacturerSpecificData = null; + + try { + while (currentPos < scanRecord.length) { + // length is unsigned int. + int length = scanRecord[currentPos++] & 0xFF; + if (length == 0) { + break; + } + // Note the length includes the length of the field type itself. + int dataLength = length - 1; + // fieldType is unsigned int. + int fieldType = scanRecord[currentPos++] & 0xFF; + switch (fieldType) { + case DATA_TYPE_FLAGS: + advertiseFlag = scanRecord[currentPos] & 0xFF; + break; + case DATA_TYPE_SERVICE_UUIDS_16_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_16_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, + dataLength, BluetoothUuid.UUID_BYTES_16_BIT, serviceUuids); + break; + case DATA_TYPE_SERVICE_UUIDS_32_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_32_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, dataLength, + BluetoothUuid.UUID_BYTES_32_BIT, serviceUuids); + break; + case DATA_TYPE_SERVICE_UUIDS_128_BIT_PARTIAL: + case DATA_TYPE_SERVICE_UUIDS_128_BIT_COMPLETE: + parseServiceUuid(scanRecord, currentPos, dataLength, + BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids); + break; + case DATA_TYPE_LOCAL_NAME_SHORT: + case DATA_TYPE_LOCAL_NAME_COMPLETE: + localName = new String( + extractBytes(scanRecord, currentPos, dataLength)); + break; + case DATA_TYPE_TX_POWER_LEVEL: + txPowerLevel = scanRecord[currentPos]; + break; + case DATA_TYPE_SERVICE_DATA: + serviceData = extractBytes(scanRecord, currentPos, dataLength); + // The first two bytes of the service data are service data uuid. + int serviceUuidLength = BluetoothUuid.UUID_BYTES_16_BIT; + byte[] serviceDataUuidBytes = extractBytes(scanRecord, currentPos, + serviceUuidLength); + serviceDataUuid = BluetoothUuid.parseUuidFrom(serviceDataUuidBytes); + break; + case DATA_TYPE_MANUFACTURER_SPECIFIC_DATA: + manufacturerSpecificData = extractBytes(scanRecord, currentPos, + dataLength); + // The first two bytes of the manufacturer specific data are + // manufacturer ids in little endian. + manufacturerId = ((manufacturerSpecificData[1] & 0xFF) << 8) + + (manufacturerSpecificData[0] & 0xFF); + break; + default: + // Just ignore, we don't handle such data type. + break; + } + currentPos += dataLength; + } + + if (serviceUuids.isEmpty()) { + serviceUuids = null; + } + return new ScanRecord(serviceUuids, serviceDataUuid, serviceData, + manufacturerId, manufacturerSpecificData, advertiseFlag, txPowerLevel, + localName); + } catch (IndexOutOfBoundsException e) { + Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord)); + return null; + } + } + + // Parse service uuids. + private static int parseServiceUuid(byte[] scanRecord, int currentPos, int dataLength, + int uuidLength, List<ParcelUuid> serviceUuids) { + while (dataLength > 0) { + byte[] uuidBytes = extractBytes(scanRecord, currentPos, + uuidLength); + serviceUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes)); + dataLength -= uuidLength; + currentPos += uuidLength; + } + return currentPos; + } + + // Helper method to extract bytes from byte array. + private static byte[] extractBytes(byte[] scanRecord, int start, int length) { + byte[] bytes = new byte[length]; + System.arraycopy(scanRecord, start, bytes, 0, length); + return bytes; + } +} diff --git a/core/java/android/bluetooth/le/ScanResult.aidl b/core/java/android/bluetooth/le/ScanResult.aidl new file mode 100644 index 0000000..3943035 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanResult.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.bluetooth.le; + +parcelable ScanResult;
\ No newline at end of file diff --git a/core/java/android/bluetooth/le/ScanResult.java b/core/java/android/bluetooth/le/ScanResult.java new file mode 100644 index 0000000..7e6e8f8 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanResult.java @@ -0,0 +1,162 @@ +/* + * 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.bluetooth.le; + +import android.annotation.Nullable; +import android.bluetooth.BluetoothDevice; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Arrays; +import java.util.Objects; + +/** + * ScanResult for Bluetooth LE scan. + */ +public final class ScanResult implements Parcelable { + // Remote bluetooth device. + private BluetoothDevice mDevice; + + // Scan record, including advertising data and scan response data. + private byte[] mScanRecord; + + // Received signal strength. + private int mRssi; + + // Device timestamp when the result was last seen. + private long mTimestampNanos; + + /** + * Constructor of scan result. + * + * @hide + */ + public ScanResult(BluetoothDevice device, byte[] scanRecord, int rssi, + long timestampNanos) { + mDevice = device; + mScanRecord = scanRecord; + mRssi = rssi; + mTimestampNanos = timestampNanos; + } + + private ScanResult(Parcel in) { + readFromParcel(in); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mDevice != null) { + dest.writeInt(1); + mDevice.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + if (mScanRecord != null) { + dest.writeInt(1); + dest.writeByteArray(mScanRecord); + } else { + dest.writeInt(0); + } + dest.writeInt(mRssi); + dest.writeLong(mTimestampNanos); + } + + private void readFromParcel(Parcel in) { + if (in.readInt() == 1) { + mDevice = BluetoothDevice.CREATOR.createFromParcel(in); + } + if (in.readInt() == 1) { + mScanRecord = in.createByteArray(); + } + mRssi = in.readInt(); + mTimestampNanos = in.readLong(); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Returns the remote bluetooth device identified by the bluetooth device address. + */ + @Nullable + public BluetoothDevice getDevice() { + return mDevice; + } + + /** + * Returns the scan record, which can be a combination of advertisement and scan response. + */ + @Nullable + public byte[] getScanRecord() { + return mScanRecord; + } + + /** + * Returns the received signal strength in dBm. The valid range is [-127, 127]. + */ + public int getRssi() { + return mRssi; + } + + /** + * Returns timestamp since boot when the scan record was observed. + */ + public long getTimestampNanos() { + return mTimestampNanos; + } + + @Override + public int hashCode() { + return Objects.hash(mDevice, mRssi, mScanRecord, mTimestampNanos); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ScanResult other = (ScanResult) obj; + return Objects.equals(mDevice, other.mDevice) && (mRssi == other.mRssi) && + Objects.deepEquals(mScanRecord, other.mScanRecord) + && (mTimestampNanos == other.mTimestampNanos); + } + + @Override + public String toString() { + return "ScanResult{" + "mDevice=" + mDevice + ", mScanRecord=" + + Arrays.toString(mScanRecord) + ", mRssi=" + mRssi + ", mTimestampNanos=" + + mTimestampNanos + '}'; + } + + public static final Parcelable.Creator<ScanResult> CREATOR = new Creator<ScanResult>() { + @Override + public ScanResult createFromParcel(Parcel source) { + return new ScanResult(source); + } + + @Override + public ScanResult[] newArray(int size) { + return new ScanResult[size]; + } + }; + +} diff --git a/core/java/android/bluetooth/le/ScanSettings.aidl b/core/java/android/bluetooth/le/ScanSettings.aidl new file mode 100644 index 0000000..eb169c1 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanSettings.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.bluetooth.le; + +parcelable ScanSettings; diff --git a/core/java/android/bluetooth/le/ScanSettings.java b/core/java/android/bluetooth/le/ScanSettings.java new file mode 100644 index 0000000..0a85675 --- /dev/null +++ b/core/java/android/bluetooth/le/ScanSettings.java @@ -0,0 +1,221 @@ +/* + * 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.bluetooth.le; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Settings for Bluetooth LE scan. + */ +public final class ScanSettings implements Parcelable { + /** + * Perform Bluetooth LE scan in low power mode. This is the default scan mode as it consumes the + * least power. + */ + public static final int SCAN_MODE_LOW_POWER = 0; + /** + * Perform Bluetooth LE scan in balanced power mode. + */ + public static final int SCAN_MODE_BALANCED = 1; + /** + * Scan using highest duty cycle. It's recommended only using this mode when the application is + * running in foreground. + */ + public static final int SCAN_MODE_LOW_LATENCY = 2; + + /** + * Callback each time when a bluetooth advertisement is found. + */ + public static final int CALLBACK_TYPE_ON_UPDATE = 0; + /** + * Callback when a bluetooth advertisement is found for the first time. + * + * @hide + */ + public static final int CALLBACK_TYPE_ON_FOUND = 1; + /** + * Callback when a bluetooth advertisement is found for the first time, then lost. + * + * @hide + */ + public static final int CALLBACK_TYPE_ON_LOST = 2; + + /** + * Full scan result which contains device mac address, rssi, advertising and scan response and + * scan timestamp. + */ + public static final int SCAN_RESULT_TYPE_FULL = 0; + /** + * Truncated scan result which contains device mac address, rssi and scan timestamp. Note it's + * possible for an app to get more scan results that it asks if there are multiple apps using + * this type. TODO: decide whether we could unhide this setting. + * + * @hide + */ + public static final int SCAN_RESULT_TYPE_TRUNCATED = 1; + + // Bluetooth LE scan mode. + private int mScanMode; + + // Bluetooth LE scan callback type + private int mCallbackType; + + // Bluetooth LE scan result type + private int mScanResultType; + + // Time of delay for reporting the scan result + private long mReportDelayNanos; + + public int getScanMode() { + return mScanMode; + } + + public int getCallbackType() { + return mCallbackType; + } + + public int getScanResultType() { + return mScanResultType; + } + + /** + * Returns report delay timestamp based on the device clock. + */ + public long getReportDelayNanos() { + return mReportDelayNanos; + } + + private ScanSettings(int scanMode, int callbackType, int scanResultType, + long reportDelayNanos) { + mScanMode = scanMode; + mCallbackType = callbackType; + mScanResultType = scanResultType; + mReportDelayNanos = reportDelayNanos; + } + + private ScanSettings(Parcel in) { + mScanMode = in.readInt(); + mCallbackType = in.readInt(); + mScanResultType = in.readInt(); + mReportDelayNanos = in.readLong(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mScanMode); + dest.writeInt(mCallbackType); + dest.writeInt(mScanResultType); + dest.writeLong(mReportDelayNanos); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Parcelable.Creator<ScanSettings> + CREATOR = new Creator<ScanSettings>() { + @Override + public ScanSettings[] newArray(int size) { + return new ScanSettings[size]; + } + + @Override + public ScanSettings createFromParcel(Parcel in) { + return new ScanSettings(in); + } + }; + + /** + * Builder for {@link ScanSettings}. + */ + public static final class Builder { + private int mScanMode = SCAN_MODE_LOW_POWER; + private int mCallbackType = CALLBACK_TYPE_ON_UPDATE; + private int mScanResultType = SCAN_RESULT_TYPE_FULL; + private long mReportDelayNanos = 0; + + /** + * Set scan mode for Bluetooth LE scan. + * + * @param scanMode The scan mode can be one of + * {@link ScanSettings#SCAN_MODE_LOW_POWER}, + * {@link ScanSettings#SCAN_MODE_BALANCED} or + * {@link ScanSettings#SCAN_MODE_LOW_LATENCY}. + * @throws IllegalArgumentException If the {@code scanMode} is invalid. + */ + public Builder setScanMode(int scanMode) { + if (scanMode < SCAN_MODE_LOW_POWER || scanMode > SCAN_MODE_LOW_LATENCY) { + throw new IllegalArgumentException("invalid scan mode " + scanMode); + } + mScanMode = scanMode; + return this; + } + + /** + * Set callback type for Bluetooth LE scan. + * + * @param callbackType The callback type for the scan. Can only be + * {@link ScanSettings#CALLBACK_TYPE_ON_UPDATE}. + * @throws IllegalArgumentException If the {@code callbackType} is invalid. + */ + public Builder setCallbackType(int callbackType) { + if (callbackType < CALLBACK_TYPE_ON_UPDATE + || callbackType > CALLBACK_TYPE_ON_LOST) { + throw new IllegalArgumentException("invalid callback type - " + callbackType); + } + mCallbackType = callbackType; + return this; + } + + /** + * Set scan result type for Bluetooth LE scan. + * + * @param scanResultType Type for scan result, could be either + * {@link ScanSettings#SCAN_RESULT_TYPE_FULL} or + * {@link ScanSettings#SCAN_RESULT_TYPE_TRUNCATED}. + * @throws IllegalArgumentException If the {@code scanResultType} is invalid. + * @hide + */ + public Builder setScanResultType(int scanResultType) { + if (scanResultType < SCAN_RESULT_TYPE_FULL + || scanResultType > SCAN_RESULT_TYPE_TRUNCATED) { + throw new IllegalArgumentException( + "invalid scanResultType - " + scanResultType); + } + mScanResultType = scanResultType; + return this; + } + + /** + * Set report delay timestamp for Bluetooth LE scan. + */ + public Builder setReportDelayNanos(long reportDelayNanos) { + mReportDelayNanos = reportDelayNanos; + return this; + } + + /** + * Build {@link ScanSettings}. + */ + public ScanSettings build() { + return new ScanSettings(mScanMode, mCallbackType, mScanResultType, + mReportDelayNanos); + } + } +} diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index 50c4fed..b44abf9 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -16,6 +16,7 @@ package android.content; +import static android.content.ContentProvider.maybeAddUserId; import android.content.res.AssetFileDescriptor; import android.graphics.Bitmap; import android.net.Uri; @@ -186,7 +187,7 @@ public class ClipData implements Parcelable { final CharSequence mText; final String mHtmlText; final Intent mIntent; - final Uri mUri; + Uri mUri; /** * Create an Item consisting of a single block of (possibly styled) text. @@ -809,6 +810,24 @@ public class ClipData implements Parcelable { } } + /** + * Prepare this {@link ClipData} to leave an app process. + * + * @hide + */ + public void prepareToLeaveUser(int userId) { + final int size = mItems.size(); + for (int i = 0; i < size; i++) { + final Item item = mItems.get(i); + if (item.mIntent != null) { + item.mIntent.prepareToLeaveUser(userId); + } + if (item.mUri != null) { + item.mUri = maybeAddUserId(item.mUri, userId); + } + } + } + @Override public String toString() { StringBuilder b = new StringBuilder(128); diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 02c850b..be9782f 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -36,6 +36,7 @@ import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.UserHandle; import android.util.Log; +import android.text.TextUtils; import java.io.File; import java.io.FileDescriptor; @@ -195,6 +196,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { return rejectQuery(uri, projection, selection, selectionArgs, sortOrder, CancellationSignal.fromTransport(cancellationSignal)); } + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.query( @@ -207,6 +209,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { @Override public String getType(Uri uri) { + uri = getUriWithoutUserId(uri); return ContentProvider.this.getType(uri); } @@ -215,9 +218,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return rejectInsert(uri, initialValues); } + int userId = getUserIdFromUri(uri); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { - return ContentProvider.this.insert(uri, initialValues); + return maybeAddUserId(ContentProvider.this.insert(uri, initialValues), userId); } finally { setCallingPackage(original); } @@ -228,6 +233,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return 0; } + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.bulkInsert(uri, initialValues); @@ -240,24 +246,39 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public ContentProviderResult[] applyBatch(String callingPkg, ArrayList<ContentProviderOperation> operations) throws OperationApplicationException { - for (ContentProviderOperation operation : operations) { + int numOperations = operations.size(); + final int[] userIds = new int[numOperations]; + for (int i = 0; i < numOperations; i++) { + ContentProviderOperation operation = operations.get(i); + userIds[i] = getUserIdFromUri(operation.getUri()); if (operation.isReadOperation()) { if (enforceReadPermission(callingPkg, operation.getUri()) != AppOpsManager.MODE_ALLOWED) { throw new OperationApplicationException("App op not allowed", 0); } } - if (operation.isWriteOperation()) { if (enforceWritePermission(callingPkg, operation.getUri()) != AppOpsManager.MODE_ALLOWED) { throw new OperationApplicationException("App op not allowed", 0); } } + if (userIds[i] != UserHandle.USER_CURRENT) { + // Removing the user id from the uri. + operation = new ContentProviderOperation(operation, true); + } + operations.set(i, operation); } final String original = setCallingPackage(callingPkg); try { - return ContentProvider.this.applyBatch(operations); + ContentProviderResult[] results = ContentProvider.this.applyBatch(operations); + for (int i = 0; i < results.length ; i++) { + if (userIds[i] != UserHandle.USER_CURRENT) { + // Adding the userId to the uri. + results[i] = new ContentProviderResult(results[i], userIds[i]); + } + } + return results; } finally { setCallingPackage(original); } @@ -268,6 +289,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return 0; } + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.delete(uri, selection, selectionArgs); @@ -282,6 +304,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceWritePermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return 0; } + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.update(uri, values, selection, selectionArgs); @@ -295,6 +318,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { String callingPkg, Uri uri, String mode, ICancellationSignal cancellationSignal) throws FileNotFoundException { enforceFilePermission(callingPkg, uri, mode); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.openFile( @@ -309,6 +333,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { String callingPkg, Uri uri, String mode, ICancellationSignal cancellationSignal) throws FileNotFoundException { enforceFilePermission(callingPkg, uri, mode); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.openAssetFile( @@ -330,6 +355,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { @Override public String[] getStreamTypes(Uri uri, String mimeTypeFilter) { + uri = getUriWithoutUserId(uri); return ContentProvider.this.getStreamTypes(uri, mimeTypeFilter); } @@ -337,6 +363,7 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public AssetFileDescriptor openTypedAssetFile(String callingPkg, Uri uri, String mimeType, Bundle opts, ICancellationSignal cancellationSignal) throws FileNotFoundException { enforceFilePermission(callingPkg, uri, "r"); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { return ContentProvider.this.openTypedAssetFile( @@ -356,9 +383,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return null; } + int userId = getUserIdFromUri(uri); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { - return ContentProvider.this.canonicalize(uri); + return maybeAddUserId(ContentProvider.this.canonicalize(uri), userId); } finally { setCallingPackage(original); } @@ -369,9 +398,11 @@ public abstract class ContentProvider implements ComponentCallbacks2 { if (enforceReadPermission(callingPkg, uri) != AppOpsManager.MODE_ALLOWED) { return null; } + int userId = getUserIdFromUri(uri); + uri = getUriWithoutUserId(uri); final String original = setCallingPackage(callingPkg); try { - return ContentProvider.this.uncanonicalize(uri); + return maybeAddUserId(ContentProvider.this.uncanonicalize(uri), userId); } finally { setCallingPackage(original); } @@ -1680,4 +1711,75 @@ public abstract class ContentProvider implements ComponentCallbacks2 { public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { writer.println("nothing to dump"); } + + /** @hide */ + public static int getUserIdFromAuthority(String auth, int defaultUserId) { + if (auth == null) return defaultUserId; + int end = auth.indexOf('@'); + if (end == -1) return defaultUserId; + String userIdString = auth.substring(0, end); + try { + return Integer.parseInt(userIdString); + } catch (NumberFormatException e) { + Log.w(TAG, "Error parsing userId.", e); + return UserHandle.USER_NULL; + } + } + + /** @hide */ + public static int getUserIdFromAuthority(String auth) { + return getUserIdFromAuthority(auth, UserHandle.USER_CURRENT); + } + + /** @hide */ + public static int getUserIdFromUri(Uri uri, int defaultUserId) { + if (uri == null) return defaultUserId; + return getUserIdFromAuthority(uri.getAuthority(), defaultUserId); + } + + /** @hide */ + public static int getUserIdFromUri(Uri uri) { + return getUserIdFromUri(uri, UserHandle.USER_CURRENT); + } + + /** + * Removes userId part from authority string. Expects format: + * userId@some.authority + * If there is no userId in the authority, it symply returns the argument + * @hide + */ + public static String getAuthorityWithoutUserId(String auth) { + if (auth == null) return null; + int end = auth.indexOf('@'); + return auth.substring(end+1); + } + + /** @hide */ + public static Uri getUriWithoutUserId(Uri uri) { + if (uri == null) return null; + Uri.Builder builder = uri.buildUpon(); + builder.authority(getAuthorityWithoutUserId(uri.getAuthority())); + return builder.build(); + } + + /** @hide */ + public static boolean uriHasUserId(Uri uri) { + if (uri == null) return false; + return !TextUtils.isEmpty(uri.getUserInfo()); + } + + /** @hide */ + public static Uri maybeAddUserId(Uri uri, int userId) { + if (uri == null) return null; + if (userId != UserHandle.USER_CURRENT + && ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) { + if (!uriHasUserId(uri)) { + //We don't add the user Id if there's already one + Uri.Builder builder = uri.buildUpon(); + builder.encodedAuthority("" + userId + "@" + uri.getEncodedAuthority()); + return builder.build(); + } + } + return uri; + } } diff --git a/core/java/android/content/ContentProviderOperation.java b/core/java/android/content/ContentProviderOperation.java index 12e9bab..136e54d 100644 --- a/core/java/android/content/ContentProviderOperation.java +++ b/core/java/android/content/ContentProviderOperation.java @@ -16,6 +16,7 @@ package android.content; +import android.content.ContentProvider; import android.database.Cursor; import android.net.Uri; import android.os.Parcel; @@ -87,6 +88,31 @@ public class ContentProviderOperation implements Parcelable { mYieldAllowed = source.readInt() != 0; } + /** @hide */ + public ContentProviderOperation(ContentProviderOperation cpo, boolean removeUserIdFromUri) { + mType = cpo.mType; + if (removeUserIdFromUri) { + mUri = ContentProvider.getUriWithoutUserId(cpo.mUri); + } else { + mUri = cpo.mUri; + } + mValues = cpo.mValues; + mSelection = cpo.mSelection; + mSelectionArgs = cpo.mSelectionArgs; + mExpectedCount = cpo.mExpectedCount; + mSelectionArgsBackReferences = cpo.mSelectionArgsBackReferences; + mValuesBackReferences = cpo.mValuesBackReferences; + mYieldAllowed = cpo.mYieldAllowed; + } + + /** @hide */ + public ContentProviderOperation getWithoutUserIdInUri() { + if (ContentProvider.uriHasUserId(mUri)) { + return new ContentProviderOperation(this, true); + } + return this; + } + public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mType); Uri.writeToParcel(dest, mUri); @@ -387,7 +413,6 @@ public class ContentProviderOperation implements Parcelable { } }; - /** * Used to add parameters to a {@link ContentProviderOperation}. The {@link Builder} is * first created by calling {@link ContentProviderOperation#newInsert(android.net.Uri)}, diff --git a/core/java/android/content/ContentProviderResult.java b/core/java/android/content/ContentProviderResult.java index 5d188ef..ec3d002 100644 --- a/core/java/android/content/ContentProviderResult.java +++ b/core/java/android/content/ContentProviderResult.java @@ -16,7 +16,9 @@ package android.content; +import android.content.ContentProvider; import android.net.Uri; +import android.os.UserHandle; import android.os.Parcelable; import android.os.Parcel; @@ -50,6 +52,12 @@ public class ContentProviderResult implements Parcelable { } } + /** @hide */ + public ContentProviderResult(ContentProviderResult cpr, int userId) { + uri = ContentProvider.maybeAddUserId(cpr.uri, userId); + count = cpr.count; + } + public void writeToParcel(Parcel dest, int flags) { if (uri == null) { dest.writeInt(1); @@ -81,4 +89,4 @@ public class ContentProviderResult implements Parcelable { } return "ContentProviderResult(count=" + count + ")"; } -}
\ No newline at end of file +} diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 5b41394..7642e13 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -1643,7 +1643,8 @@ public abstract class ContentResolver { */ public void takePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) { try { - ActivityManagerNative.getDefault().takePersistableUriPermission(uri, modeFlags); + ActivityManagerNative.getDefault().takePersistableUriPermission( + ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri)); } catch (RemoteException e) { } } @@ -1658,7 +1659,8 @@ public abstract class ContentResolver { */ public void releasePersistableUriPermission(Uri uri, @Intent.AccessUriMode int modeFlags) { try { - ActivityManagerNative.getDefault().releasePersistableUriPermission(uri, modeFlags); + ActivityManagerNative.getDefault().releasePersistableUriPermission( + ContentProvider.getUriWithoutUserId(uri), modeFlags, resolveUserId(uri)); } catch (RemoteException e) { } } @@ -2462,4 +2464,9 @@ public abstract class ContentResolver { private final Context mContext; final String mPackageName; private static final String TAG = "ContentResolver"; + + /** @hide */ + public int resolveUserId(Uri uri) { + return ContentProvider.getUserIdFromUri(uri, mContext.getUserId()); + } } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index a059e48..a040efb 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -40,6 +40,7 @@ import android.os.Looper; import android.os.StatFs; import android.os.UserHandle; import android.os.UserManager; +import android.provider.MediaStore; import android.util.AttributeSet; import android.view.DisplayAdjustments; import android.view.Display; @@ -929,6 +930,40 @@ public abstract class Context { public abstract File[] getExternalCacheDirs(); /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application can place media files. + * These files are scanned and made available to other apps through + * {@link MediaStore}. + * <p> + * This is like {@link #getExternalFilesDirs} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + * <ul> + * <li>External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + * <li>There is no security enforced with these files. + * </ul> + * <p> + * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + * <p> + * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + * <p> + * No permissions are required to read or write to the returned paths; they + * are always accessible to the calling app. Write access outside of these + * paths on secondary external storage devices is not available. + * <p> + * Returned paths may be {@code null} if a storage device is unavailable. + * + * @see Environment#getExternalStorageState(File) + */ + public abstract File[] getExternalMediaDirs(); + + /** * Returns an array of strings naming the private files associated with * this Context's application package. * @@ -1499,6 +1534,17 @@ public abstract class Context { @Nullable Bundle initialExtras); /** + * Similar to above but takes an appOp as well, to enforce restrictions. + * @see #sendOrderedBroadcastAsUser(Intent, UserHandle, String, + * BroadcastReceiver, Handler, int, String, Bundle) + * @hide + */ + public abstract void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, + @Nullable String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + @Nullable Handler scheduler, int initialCode, @Nullable String initialData, + @Nullable Bundle initialExtras); + + /** * Perform a {@link #sendBroadcast(Intent)} that is "sticky," meaning the * Intent you are sending stays around after the broadcast is complete, * so that others can quickly retrieve that data through the return @@ -1980,9 +2026,10 @@ public abstract class Context { //@hide: NETWORK_STATS_SERVICE, //@hide: NETWORK_POLICY_SERVICE, WIFI_SERVICE, - WIFI_HOTSPOT_SERVICE, + WIFI_PASSPOINT_SERVICE, WIFI_P2P_SERVICE, WIFI_SCANNING_SERVICE, + //@hide: ETHERNET_SERVICE, NSD_SERVICE, AUDIO_SERVICE, MEDIA_ROUTER_SERVICE, @@ -2011,6 +2058,7 @@ public abstract class Context { PRINT_SERVICE, MEDIA_SESSION_SERVICE, BATTERY_SERVICE, + TASK_SERVICE, }) @Retention(RetentionPolicy.SOURCE) public @interface ServiceName {} @@ -2067,6 +2115,8 @@ public abstract class Context { * <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 + * <dt> {@link #TASK_SERVICE} ("taskmanager") + * <dd> A {@link android.app.task.TaskManager} for managing scheduled tasks * </dl> * * <p>Note: System services obtained via this API may be closely associated with @@ -2122,6 +2172,8 @@ public abstract class Context { * @see android.app.DownloadManager * @see #BATTERY_SERVICE * @see android.os.BatteryManager + * @see #TASK_SERVICE + * @see android.app.task.TaskManager */ public abstract Object getSystemService(@ServiceName @NonNull String name); @@ -2341,13 +2393,14 @@ 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. + * android.net.wifi.passpoint.WifiPasspointManager} for handling management of + * Wi-Fi passpoint access. * * @see #getSystemService - * @see android.net.wifi.hotspot.WifiHotspotManager + * @see android.net.wifi.passpoint.WifiPasspointManager + * @hide */ - public static final String WIFI_HOTSPOT_SERVICE = "wifihotspot"; + public static final String WIFI_PASSPOINT_SERVICE = "wifipasspoint"; /** * Use with {@link #getSystemService} to retrieve a {@link @@ -2371,6 +2424,18 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a {@link + * android.net.ethernet.EthernetManager} for handling management of + * Ethernet access. + * + * @see #getSystemService + * @see android.net.ethernet.EthernetManager + * + * @hide + */ + public static final String ETHERNET_SERVICE = "ethernet"; + + /** + * Use with {@link #getSystemService} to retrieve a {@link * android.net.nsd.NsdManager} for handling management of network service * discovery * @@ -2411,10 +2476,10 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@link android.media.session.SessionManager} for managing media Sessions. + * {@link android.media.session.MediaSessionManager} for managing media Sessions. * * @see #getSystemService - * @see android.media.session.SessionManager + * @see android.media.session.MediaSessionManager */ public static final String MEDIA_SESSION_SERVICE = "media_session"; @@ -2572,13 +2637,24 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@link android.hardware.hdmi.HdmiCecManager for controlling and managing + * {@link android.hardware.hdmi.HdmiCecManager} for controlling and managing * HDMI-CEC protocol. * * @see #getSystemService * @see android.hardware.hdmi.HdmiCecManager */ - public static final String HDMI_CEC_SERVICE = "hdmi_cec"; + // TODO: Remove this once HdmiControlService is ready. + public static final String HDMI_CEC_SERVICE = "hdmi_cec"; + + /** + * Use with {@link #getSystemService} to retrieve a + * {@link android.hardware.hdmi.HdmiControlManager} for controlling and managing + * HDMI-CEC protocol. + * + * @see #getSystemService + * @see android.hardware.hdmi.HdmiControlManager + */ + public static final String HDMI_CONTROL_SERVICE = "hdmi_control"; /** * Use with {@link #getSystemService} to retrieve a @@ -2619,6 +2695,15 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a + * {@link android.content.RestrictionsManager} for retrieving application restrictions + * and requesting permissions for restricted operations. + * @see #getSystemService + * @see android.content.RestrictionsManager + */ + public static final String RESTRICTIONS_SERVICE = "restrictions"; + + /** + * Use with {@link #getSystemService} to retrieve a * {@link android.app.AppOpsManager} for tracking application operations * on the device. * @@ -2666,11 +2751,11 @@ public abstract class Context { /** * Use with {@link #getSystemService} to retrieve a - * {@link android.tv.TvInputManager} for interacting with TV inputs on the - * device. + * {@link android.media.tv.TvInputManager} for interacting with TV inputs + * on the device. * * @see #getSystemService - * @see android.tv.TvInputManager + * @see android.media.tv.TvInputManager */ public static final String TV_INPUT_SERVICE = "tv_input"; @@ -2693,6 +2778,15 @@ public abstract class Context { public static final String USAGE_STATS_SERVICE = "usagestats"; /** + * Use with {@link #getSystemService} to retrieve a {@link + * android.app.task.TaskManager} instance for managing occasional + * background tasks. + * @see #getSystemService + * @see android.app.task.TaskManager + */ + public static final String TASK_SERVICE = "task"; + + /** * Determine whether the given permission is allowed for a particular * process and user ID running in the system. * diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 93f6cdf..dbf9122 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -237,6 +237,11 @@ public class ContextWrapper extends Context { } @Override + public File[] getExternalMediaDirs() { + return mBase.getExternalMediaDirs(); + } + + @Override public File getDir(String name, int mode) { return mBase.getDir(name, mode); } @@ -418,6 +423,16 @@ public class ContextWrapper extends Context { scheduler, initialCode, initialData, initialExtras); } + /** @hide */ + @Override + public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, + String receiverPermission, int appOp, BroadcastReceiver resultReceiver, + Handler scheduler, + int initialCode, String initialData, Bundle initialExtras) { + mBase.sendOrderedBroadcastAsUser(intent, user, receiverPermission, appOp, resultReceiver, + scheduler, initialCode, initialData, initialExtras); + } + @Override public void sendStickyBroadcast(Intent intent) { mBase.sendStickyBroadcast(intent); diff --git a/core/java/android/content/IRestrictionsManager.aidl b/core/java/android/content/IRestrictionsManager.aidl new file mode 100644 index 0000000..b1c0a3a --- /dev/null +++ b/core/java/android/content/IRestrictionsManager.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; + +import android.os.Bundle; + +/** + * Interface used by the RestrictionsManager + * @hide + */ +interface IRestrictionsManager { + Bundle getApplicationRestrictions(in String packageName); + boolean hasRestrictionsProvider(); + void requestPermission(in String packageName, in String requestTemplate, in Bundle requestData); + void notifyPermissionResponse(in String packageName, in Bundle response); +} diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 3cfc56c..bd07470 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -26,6 +26,7 @@ import android.annotation.IntDef; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.content.pm.ActivityInfo; +import static android.content.ContentProvider.maybeAddUserId; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; @@ -44,6 +45,7 @@ import android.util.AttributeSet; import android.util.Log; import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.Serializable; @@ -603,6 +605,15 @@ import java.util.Set; * of all possible flags. */ public class Intent implements Parcelable, Cloneable { + private static final String ATTR_ACTION = "action"; + private static final String TAG_CATEGORIES = "categories"; + private static final String ATTR_CATEGORY = "category"; + private static final String TAG_EXTRA = "extra"; + private static final String ATTR_TYPE = "type"; + private static final String ATTR_COMPONENT = "component"; + private static final String ATTR_DATA = "data"; + private static final String ATTR_FLAGS = "flags"; + // --------------------------------------------------------------------- // --------------------------------------------------------------------- // Standard intent activity actions (see action variable). @@ -3728,32 +3739,27 @@ 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} <code>standard</code> - * or <code>singleTop</code>. + * This flag is used to open a document into a new task rooted at the activity launched + * by this Intent. Through the use of this flag, or its equivalent attribute, + * {@link android.R.attr#documentLaunchMode} multiple instances of the same activity + * containing different douments will appear in the recent tasks list. * - * <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>The use of the activity attribute form of this, + * {@link android.R.attr#documentLaunchMode}, is + * preferred over the Intent flag described here. The attribute form allows the + * Activity to specify multiple document behavior for all launchers of the Activity + * whereas using this flag requires each Intent that launches the Activity to specify it. * - * <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. + * <p>FLAG_ACTIVITY_NEW_DOCUMENT may be used in conjunction with {@link + * #FLAG_ACTIVITY_MULTIPLE_TASK}. When used alone it is the + * equivalent of the Activity manifest specifying {@link + * android.R.attr#documentLaunchMode}="intoExisting". When used with + * FLAG_ACTIVITY_MULTIPLE_TASK it is the equivalent of the Activity manifest specifying + * {@link android.R.attr#documentLaunchMode}="always". * - * <p>This is equivalent to the attribute {@link android.R.attr#documentLaunchMode}. + * Refer to {@link android.R.attr#documentLaunchMode} for more information. * + * @see android.R.attr#documentLaunchMode * @see #FLAG_ACTIVITY_MULTIPLE_TASK */ public static final int FLAG_ACTIVITY_NEW_DOCUMENT = @@ -7346,7 +7352,7 @@ public class Intent implements Parcelable, Cloneable { } String nodeName = parser.getName(); - if (nodeName.equals("category")) { + if (nodeName.equals(TAG_CATEGORIES)) { sa = resources.obtainAttributes(attrs, com.android.internal.R.styleable.IntentCategory); String cat = sa.getString(com.android.internal.R.styleable.IntentCategory_name); @@ -7357,11 +7363,11 @@ public class Intent implements Parcelable, Cloneable { } XmlUtils.skipCurrentTag(parser); - } else if (nodeName.equals("extra")) { + } else if (nodeName.equals(TAG_EXTRA)) { if (intent.mExtras == null) { intent.mExtras = new Bundle(); } - resources.parseBundleExtra("extra", attrs, intent.mExtras); + resources.parseBundleExtra(TAG_EXTRA, attrs, intent.mExtras); XmlUtils.skipCurrentTag(parser); } else { @@ -7372,6 +7378,76 @@ public class Intent implements Parcelable, Cloneable { return intent; } + /** @hide */ + public void saveToXml(XmlSerializer out) throws IOException { + if (mAction != null) { + out.attribute(null, ATTR_ACTION, mAction); + } + if (mData != null) { + out.attribute(null, ATTR_DATA, mData.toString()); + } + if (mType != null) { + out.attribute(null, ATTR_TYPE, mType); + } + if (mComponent != null) { + out.attribute(null, ATTR_COMPONENT, mComponent.flattenToShortString()); + } + out.attribute(null, ATTR_FLAGS, Integer.toHexString(getFlags())); + + if (mCategories != null) { + out.startTag(null, TAG_CATEGORIES); + for (int categoryNdx = mCategories.size() - 1; categoryNdx >= 0; --categoryNdx) { + out.attribute(null, ATTR_CATEGORY, mCategories.valueAt(categoryNdx)); + } + } + } + + /** @hide */ + public static Intent restoreFromXml(XmlPullParser in) throws IOException, + XmlPullParserException { + Intent intent = new Intent(); + final int outerDepth = in.getDepth(); + + int attrCount = in.getAttributeCount(); + for (int attrNdx = attrCount - 1; attrNdx >= 0; --attrNdx) { + final String attrName = in.getAttributeName(attrNdx); + final String attrValue = in.getAttributeValue(attrNdx); + if (ATTR_ACTION.equals(attrName)) { + intent.setAction(attrValue); + } else if (ATTR_DATA.equals(attrName)) { + intent.setData(Uri.parse(attrValue)); + } else if (ATTR_TYPE.equals(attrName)) { + intent.setType(attrValue); + } else if (ATTR_COMPONENT.equals(attrName)) { + intent.setComponent(ComponentName.unflattenFromString(attrValue)); + } else if (ATTR_FLAGS.equals(attrName)) { + intent.setFlags(Integer.valueOf(attrValue, 16)); + } else { + Log.e("Intent", "restoreFromXml: unknown attribute=" + attrName); + } + } + + int event; + String name; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + name = in.getName(); + if (TAG_CATEGORIES.equals(name)) { + attrCount = in.getAttributeCount(); + for (int attrNdx = attrCount - 1; attrNdx >= 0; --attrNdx) { + intent.addCategory(in.getAttributeValue(attrNdx)); + } + } else { + Log.w("Intent", "restoreFromXml: unknown name=" + name); + XmlUtils.skipCurrentTag(in); + } + } + } + + return intent; + } + /** * Normalize a MIME data type. * @@ -7433,6 +7509,41 @@ public class Intent implements Parcelable, Cloneable { } /** + * Prepare this {@link Intent} to be sent to another user + * + * @hide + */ + public void prepareToLeaveUser(int userId) { + Uri data = getData(); + if (data != null) { + mData = maybeAddUserId(data, userId); + } + if (mSelector != null) { + mSelector.prepareToLeaveUser(userId); + } + if (mClipData != null) { + mClipData.prepareToLeaveUser(userId); + } + String action = getAction(); + if (ACTION_SEND.equals(action)) { + final Uri stream = getParcelableExtra(EXTRA_STREAM); + if (stream != null) { + putExtra(EXTRA_STREAM, maybeAddUserId(stream, userId)); + } + } + if (ACTION_SEND_MULTIPLE.equals(action)) { + final ArrayList<Uri> streams = getParcelableArrayListExtra(EXTRA_STREAM); + if (streams != null) { + ArrayList<Uri> newStreams = new ArrayList<Uri>(); + for (int i = 0; i < streams.size(); i++) { + newStreams.add(maybeAddUserId(streams.get(i), userId)); + } + putParcelableArrayListExtra(EXTRA_STREAM, newStreams); + } + } + } + + /** * Migrate any {@link #EXTRA_STREAM} in {@link #ACTION_SEND} and * {@link #ACTION_SEND_MULTIPLE} to {@link ClipData}. Also inspects nested * intents in {@link #ACTION_CHOOSER}. diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 3ff53bf..62f88a9 100644 --- a/core/java/android/content/RestrictionEntry.java +++ b/core/java/android/content/RestrictionEntry.java @@ -73,32 +73,38 @@ public class RestrictionEntry implements Parcelable { */ public static final int TYPE_MULTI_SELECT = 4; + /** + * A type of restriction. Use this for storing an integer value. The range of values + * is from {@link Integer#MIN_VALUE} to {@link Integer#MAX_VALUE}. + */ + public static final int TYPE_INTEGER = 5; + /** The type of restriction. */ - private int type; + private int mType; /** The unique key that identifies the restriction. */ - private String key; + private String mKey; /** The user-visible title of the restriction. */ - private String title; + private String mTitle; /** The user-visible secondary description of the restriction. */ - private String description; + private String mDescription; /** The user-visible set of choices used for single-select and multi-select lists. */ - private String [] choices; + private String [] mChoiceEntries; /** The values corresponding to the user-visible choices. The value(s) of this entry will * one or more of these, returned by {@link #getAllSelectedStrings()} and * {@link #getSelectedString()}. */ - private String [] values; + private String [] mChoiceValues; /* The chosen value, whose content depends on the type of the restriction. */ - private String currentValue; + private String mCurrentValue; /* List of selected choices in the multi-select case. */ - private String[] currentValues; + private String[] mCurrentValues; /** * Constructor for {@link #TYPE_CHOICE} type. @@ -106,9 +112,9 @@ public class RestrictionEntry implements Parcelable { * @param selectedString the current value */ public RestrictionEntry(String key, String selectedString) { - this.key = key; - this.type = TYPE_CHOICE; - this.currentValue = selectedString; + this.mKey = key; + this.mType = TYPE_CHOICE; + this.mCurrentValue = selectedString; } /** @@ -117,8 +123,8 @@ public class RestrictionEntry implements Parcelable { * @param selectedState whether this restriction is selected or not */ public RestrictionEntry(String key, boolean selectedState) { - this.key = key; - this.type = TYPE_BOOLEAN; + this.mKey = key; + this.mType = TYPE_BOOLEAN; setSelectedState(selectedState); } @@ -128,9 +134,20 @@ public class RestrictionEntry implements Parcelable { * @param selectedStrings the list of values that are currently selected */ public RestrictionEntry(String key, String[] selectedStrings) { - this.key = key; - this.type = TYPE_MULTI_SELECT; - this.currentValues = selectedStrings; + this.mKey = key; + this.mType = TYPE_MULTI_SELECT; + this.mCurrentValues = selectedStrings; + } + + /** + * Constructor for {@link #TYPE_INTEGER} type. + * @param key the unique key for this restriction + * @param selectedInt the integer value of the restriction + */ + public RestrictionEntry(String key, int selectedInt) { + mKey = key; + mType = TYPE_INTEGER; + setIntValue(selectedInt); } /** @@ -138,7 +155,7 @@ public class RestrictionEntry implements Parcelable { * @param type the type for this restriction. */ public void setType(int type) { - this.type = type; + this.mType = type; } /** @@ -146,7 +163,7 @@ public class RestrictionEntry implements Parcelable { * @return the type for this restriction */ public int getType() { - return type; + return mType; } /** @@ -155,7 +172,7 @@ public class RestrictionEntry implements Parcelable { * single string values. */ public String getSelectedString() { - return currentValue; + return mCurrentValue; } /** @@ -164,7 +181,7 @@ public class RestrictionEntry implements Parcelable { * null otherwise. */ public String[] getAllSelectedStrings() { - return currentValues; + return mCurrentValues; } /** @@ -172,7 +189,23 @@ public class RestrictionEntry implements Parcelable { * @return the current selected state of the entry. */ public boolean getSelectedState() { - return Boolean.parseBoolean(currentValue); + return Boolean.parseBoolean(mCurrentValue); + } + + /** + * Returns the value of the entry as an integer when the type is {@link #TYPE_INTEGER}. + * @return the integer value of the entry. + */ + public int getIntValue() { + return Integer.parseInt(mCurrentValue); + } + + /** + * Sets the integer value of the entry when the type is {@link #TYPE_INTEGER}. + * @param value the integer value to set. + */ + public void setIntValue(int value) { + mCurrentValue = Integer.toString(value); } /** @@ -181,7 +214,7 @@ public class RestrictionEntry implements Parcelable { * @param selectedString the string value to select. */ public void setSelectedString(String selectedString) { - currentValue = selectedString; + mCurrentValue = selectedString; } /** @@ -190,7 +223,7 @@ public class RestrictionEntry implements Parcelable { * @param state the current selected state */ public void setSelectedState(boolean state) { - currentValue = Boolean.toString(state); + mCurrentValue = Boolean.toString(state); } /** @@ -199,7 +232,7 @@ public class RestrictionEntry implements Parcelable { * @param allSelectedStrings the current list of selected values. */ public void setAllSelectedStrings(String[] allSelectedStrings) { - currentValues = allSelectedStrings; + mCurrentValues = allSelectedStrings; } /** @@ -216,7 +249,7 @@ public class RestrictionEntry implements Parcelable { * @see #getAllSelectedStrings() */ public void setChoiceValues(String[] choiceValues) { - values = choiceValues; + mChoiceValues = choiceValues; } /** @@ -227,7 +260,7 @@ public class RestrictionEntry implements Parcelable { * @see #setChoiceValues(String[]) */ public void setChoiceValues(Context context, int stringArrayResId) { - values = context.getResources().getStringArray(stringArrayResId); + mChoiceValues = context.getResources().getStringArray(stringArrayResId); } /** @@ -235,7 +268,7 @@ public class RestrictionEntry implements Parcelable { * @return the list of possible values. */ public String[] getChoiceValues() { - return values; + return mChoiceValues; } /** @@ -248,7 +281,7 @@ public class RestrictionEntry implements Parcelable { * @see #setChoiceValues(String[]) */ public void setChoiceEntries(String[] choiceEntries) { - choices = choiceEntries; + mChoiceEntries = choiceEntries; } /** Sets a list of strings that will be presented as choices to the user. This is similar to @@ -257,7 +290,7 @@ public class RestrictionEntry implements Parcelable { * @param stringArrayResId the resource id of a string array containing the possible entries. */ public void setChoiceEntries(Context context, int stringArrayResId) { - choices = context.getResources().getStringArray(stringArrayResId); + mChoiceEntries = context.getResources().getStringArray(stringArrayResId); } /** @@ -265,7 +298,7 @@ public class RestrictionEntry implements Parcelable { * @return the list of choices presented to the user. */ public String[] getChoiceEntries() { - return choices; + return mChoiceEntries; } /** @@ -273,7 +306,7 @@ public class RestrictionEntry implements Parcelable { * @return the user-visible description, null if none was set earlier. */ public String getDescription() { - return description; + return mDescription; } /** @@ -283,7 +316,7 @@ public class RestrictionEntry implements Parcelable { * @param description the user-visible description string. */ public void setDescription(String description) { - this.description = description; + this.mDescription = description; } /** @@ -291,7 +324,7 @@ public class RestrictionEntry implements Parcelable { * @return the key for the restriction. */ public String getKey() { - return key; + return mKey; } /** @@ -299,7 +332,7 @@ public class RestrictionEntry implements Parcelable { * @return the user-visible title for the entry, null if none was set earlier. */ public String getTitle() { - return title; + return mTitle; } /** @@ -307,7 +340,7 @@ public class RestrictionEntry implements Parcelable { * @param title the user-visible title for the entry. */ public void setTitle(String title) { - this.title = title; + this.mTitle = title; } private boolean equalArrays(String[] one, String[] other) { @@ -324,23 +357,23 @@ public class RestrictionEntry implements Parcelable { if (!(o instanceof RestrictionEntry)) return false; final RestrictionEntry other = (RestrictionEntry) o; // Make sure that either currentValue matches or currentValues matches. - return type == other.type && key.equals(other.key) + return mType == other.mType && mKey.equals(other.mKey) && - ((currentValues == null && other.currentValues == null - && currentValue != null && currentValue.equals(other.currentValue)) + ((mCurrentValues == null && other.mCurrentValues == null + && mCurrentValue != null && mCurrentValue.equals(other.mCurrentValue)) || - (currentValue == null && other.currentValue == null - && currentValues != null && equalArrays(currentValues, other.currentValues))); + (mCurrentValue == null && other.mCurrentValue == null + && mCurrentValues != null && equalArrays(mCurrentValues, other.mCurrentValues))); } @Override public int hashCode() { int result = 17; - result = 31 * result + key.hashCode(); - if (currentValue != null) { - result = 31 * result + currentValue.hashCode(); - } else if (currentValues != null) { - for (String value : currentValues) { + result = 31 * result + mKey.hashCode(); + if (mCurrentValue != null) { + result = 31 * result + mCurrentValue.hashCode(); + } else if (mCurrentValues != null) { + for (String value : mCurrentValues) { if (value != null) { result = 31 * result + value.hashCode(); } @@ -359,14 +392,14 @@ public class RestrictionEntry implements Parcelable { } public RestrictionEntry(Parcel in) { - type = in.readInt(); - key = in.readString(); - title = in.readString(); - description = in.readString(); - choices = readArray(in); - values = readArray(in); - currentValue = in.readString(); - currentValues = readArray(in); + mType = in.readInt(); + mKey = in.readString(); + mTitle = in.readString(); + mDescription = in.readString(); + mChoiceEntries = readArray(in); + mChoiceValues = readArray(in); + mCurrentValue = in.readString(); + mCurrentValues = readArray(in); } @Override @@ -387,14 +420,14 @@ public class RestrictionEntry implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(type); - dest.writeString(key); - dest.writeString(title); - dest.writeString(description); - writeArray(dest, choices); - writeArray(dest, values); - dest.writeString(currentValue); - writeArray(dest, currentValues); + dest.writeInt(mType); + dest.writeString(mKey); + dest.writeString(mTitle); + dest.writeString(mDescription); + writeArray(dest, mChoiceEntries); + writeArray(dest, mChoiceValues); + dest.writeString(mCurrentValue); + writeArray(dest, mCurrentValues); } public static final Creator<RestrictionEntry> CREATOR = new Creator<RestrictionEntry>() { @@ -409,6 +442,6 @@ public class RestrictionEntry implements Parcelable { @Override public String toString() { - return "RestrictionsEntry {type=" + type + ", key=" + key + ", value=" + currentValue + "}"; + return "RestrictionsEntry {type=" + mType + ", key=" + mKey + ", value=" + mCurrentValue + "}"; } } diff --git a/core/java/android/content/RestrictionsManager.java b/core/java/android/content/RestrictionsManager.java new file mode 100644 index 0000000..0dd0edd --- /dev/null +++ b/core/java/android/content/RestrictionsManager.java @@ -0,0 +1,344 @@ +/* + * 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.admin.DevicePolicyManager; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.Log; + +import java.util.Collections; +import java.util.List; + +/** + * Provides a mechanism for apps to query restrictions imposed by an entity that + * manages the user. Apps can also send permission requests to a local or remote + * device administrator to override default app-specific restrictions or any other + * operation that needs explicit authorization from the administrator. + * <p> + * Apps can expose a set of restrictions via a runtime receiver mechanism or via + * static meta data in the manifest. + * <p> + * If the user has an active restrictions provider, dynamic requests can be made in + * addition to the statically imposed restrictions. Dynamic requests are app-specific + * and can be expressed via a predefined set of templates. + * <p> + * The RestrictionsManager forwards the dynamic requests to the active + * restrictions provider. The restrictions provider can respond back to requests by calling + * {@link #notifyPermissionResponse(String, Bundle)}, when + * a response is received from the administrator of the device or user + * The response is relayed back to the application via a protected broadcast, + * {@link #ACTION_PERMISSION_RESPONSE_RECEIVED}. + * <p> + * Static restrictions are specified by an XML file referenced by a meta-data attribute + * in the manifest. This enables applications as well as any web administration consoles + * to be able to read the template from the apk. + * <p> + * The syntax of the XML format is as follows: + * <pre> + * <restrictions> + * <restriction + * android:key="<key>" + * android:restrictionType="boolean|string|integer|multi-select|null" + * ... /> + * <restriction ... /> + * </restrictions> + * </pre> + * <p> + * The attributes for each restriction depend on the restriction type. + * + * @see RestrictionEntry + */ +public class RestrictionsManager { + + /** + * Broadcast intent delivered when a response is received for a permission + * request. The response is not available for later query, so the receiver + * must persist and/or immediately act upon the response. The application + * should not interrupt the user by coming to the foreground if it isn't + * currently in the foreground. It can post a notification instead, informing + * the user of a change in state. + * <p> + * For instance, if the user requested permission to make an in-app purchase, + * the app can post a notification that the request had been granted or denied, + * and allow the purchase to go through. + * <p> + * The broadcast Intent carries the following extra: + * {@link #EXTRA_RESPONSE_BUNDLE}. + */ + public static final String ACTION_PERMISSION_RESPONSE_RECEIVED = + "android.intent.action.PERMISSION_RESPONSE_RECEIVED"; + + /** + * Protected broadcast intent sent to the active restrictions provider. The intent + * contains the following extras:<p> + * <ul> + * <li>{@link #EXTRA_PACKAGE_NAME} : String; the package name of the application requesting + * permission.</li> + * <li>{@link #EXTRA_TEMPLATE_ID} : String; the template of the request.</li> + * <li>{@link #EXTRA_REQUEST_BUNDLE} : Bundle; contains the template-specific keys and values + * for the request. + * </ul> + * @see DevicePolicyManager#setRestrictionsProvider(ComponentName, ComponentName) + * @see #requestPermission(String, String, Bundle) + */ + public static final String ACTION_REQUEST_PERMISSION = + "android.intent.action.REQUEST_PERMISSION"; + + /** + * The package name of the application making the request. + */ + public static final String EXTRA_PACKAGE_NAME = "package_name"; + + /** + * The template id that specifies what kind of a request it is and may indicate + * how the request is to be presented to the administrator. Must be either one of + * the predefined templates or a custom one specified by the application that the + * restrictions provider is familiar with. + */ + public static final String EXTRA_TEMPLATE_ID = "template_id"; + + /** + * A bundle containing the details about the request. The contents depend on the + * template id. + * @see #EXTRA_TEMPLATE_ID + */ + public static final String EXTRA_REQUEST_BUNDLE = "request_bundle"; + + /** + * Contains a response from the administrator for specific request. + * The bundle contains the following information, at least: + * <ul> + * <li>{@link #REQUEST_KEY_ID}: The request id.</li> + * <li>{@link #REQUEST_KEY_DATA}: The request reference data.</li> + * </ul> + * <p> + * And depending on what the request template was, the bundle will contain the actual + * result of the request. For {@link #REQUEST_TEMPLATE_QUESTION}, the result will be in + * {@link #RESPONSE_KEY_BOOLEAN}, which is of type boolean; true if the administrator + * approved the request, false otherwise. + */ + public static final String EXTRA_RESPONSE_BUNDLE = "response_bundle"; + + + /** + * Request template that presents a simple question, with a possible title and icon. + * <p> + * Required keys are + * {@link #REQUEST_KEY_ID} and {@link #REQUEST_KEY_MESSAGE}. + * <p> + * Optional keys are + * {@link #REQUEST_KEY_DATA}, {@link #REQUEST_KEY_ICON}, {@link #REQUEST_KEY_TITLE}, + * {@link #REQUEST_KEY_APPROVE_LABEL} and {@link #REQUEST_KEY_DENY_LABEL}. + */ + public static final String REQUEST_TEMPLATE_QUESTION = "android.req_template.type.simple"; + + /** + * Key for request ID contained in the request bundle. + * <p> + * App-generated request id to identify the specific request when receiving + * a response. This value is returned in the {@link #EXTRA_RESPONSE_BUNDLE}. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_ID = "android.req_template.req_id"; + + /** + * Key for request data contained in the request bundle. + * <p> + * Optional, typically used to identify the specific data that is being referred to, + * such as the unique identifier for a movie or book. This is not used for display + * purposes and is more like a cookie. This value is returned in the + * {@link #EXTRA_RESPONSE_BUNDLE}. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_DATA = "android.req_template.data"; + + /** + * Key for request title contained in the request bundle. + * <p> + * Optional, typically used as the title of any notification or dialog presented + * to the administrator who approves the request. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_TITLE = "android.req_template.title"; + + /** + * Key for request message contained in the request bundle. + * <p> + * Required, shown as the actual message in a notification or dialog presented + * to the administrator who approves the request. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_MESSAGE = "android.req_template.mesg"; + + /** + * Key for request icon contained in the request bundle. + * <p> + * Optional, shown alongside the request message presented to the administrator + * who approves the request. + * <p> + * Type: Bitmap + */ + public static final String REQUEST_KEY_ICON = "android.req_template.icon"; + + /** + * Key for request approval button label contained in the request bundle. + * <p> + * Optional, may be shown as a label on the positive button in a dialog or + * notification presented to the administrator who approves the request. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_APPROVE_LABEL = "android.req_template.accept"; + + /** + * Key for request rejection button label contained in the request bundle. + * <p> + * Optional, may be shown as a label on the negative button in a dialog or + * notification presented to the administrator who approves the request. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_DENY_LABEL = "android.req_template.reject"; + + /** + * Key for requestor's name contained in the request bundle. This value is not specified by + * the application. It is automatically inserted into the Bundle by the Restrictions Provider + * before it is sent to the administrator. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_REQUESTOR_NAME = "android.req_template.requestor"; + + /** + * Key for requestor's device name contained in the request bundle. This value is not specified + * by the application. It is automatically inserted into the Bundle by the Restrictions Provider + * before it is sent to the administrator. + * <p> + * Type: String + */ + public static final String REQUEST_KEY_DEVICE_NAME = "android.req_template.device"; + + /** + * Key for the response in the response bundle sent to the application, for a permission + * request. + * <p> + * Type: boolean + */ + public static final String RESPONSE_KEY_BOOLEAN = "android.req_template.response"; + + private static final String TAG = "RestrictionsManager"; + + private final Context mContext; + private final IRestrictionsManager mService; + + /** + * @hide + */ + public RestrictionsManager(Context context, IRestrictionsManager service) { + mContext = context; + mService = service; + } + + /** + * Returns any available set of application-specific restrictions applicable + * to this application. + * @return + */ + public Bundle getApplicationRestrictions() { + try { + if (mService != null) { + return mService.getApplicationRestrictions(mContext.getPackageName()); + } + } catch (RemoteException re) { + Log.w(TAG, "Couldn't reach service"); + } + return null; + } + + /** + * Called by an application to check if permission requests can be made. If false, + * there is no need to request permission for an operation, unless a static + * restriction applies to that operation. + * @return + */ + public boolean hasRestrictionsProvider() { + try { + if (mService != null) { + return mService.hasRestrictionsProvider(); + } + } catch (RemoteException re) { + Log.w(TAG, "Couldn't reach service"); + } + return false; + } + + /** + * Called by an application to request permission for an operation. The contents of the + * request are passed in a Bundle that contains several pieces of data depending on the + * chosen request template. + * + * @param requestTemplate The request template to use. The template could be one of the + * predefined templates specified in this class or a custom template that the specific + * Restrictions Provider might understand. For custom templates, the template name should be + * namespaced to avoid collisions with predefined templates and templates specified by + * other Restrictions Provider vendors. + * @param requestData A Bundle containing the data corresponding to the specified request + * template. The keys for the data in the bundle depend on the kind of template chosen. + */ + public void requestPermission(String requestTemplate, Bundle requestData) { + try { + if (mService != null) { + mService.requestPermission(mContext.getPackageName(), requestTemplate, requestData); + } + } catch (RemoteException re) { + Log.w(TAG, "Couldn't reach service"); + } + } + + /** + * Called by the Restrictions Provider when a response is available to be + * delivered to an application. + * @param packageName + * @param response + */ + public void notifyPermissionResponse(String packageName, Bundle response) { + try { + if (mService != null) { + mService.notifyPermissionResponse(packageName, response); + } + } catch (RemoteException re) { + Log.w(TAG, "Couldn't reach service"); + } + } + + /** + * Parse and return the list of restrictions defined in the manifest for the specified + * package, if any. + * @param packageName The application for which to fetch the restrictions list. + * @return The list of RestrictionEntry objects created from the XML file specified + * in the manifest, or null if none was specified. + */ + public List<RestrictionEntry> getManifestRestrictions(String packageName) { + // TODO: + return null; + } +} diff --git a/core/java/android/content/SharedPreferences.java b/core/java/android/content/SharedPreferences.java index d4f7f06..46c9234 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -82,7 +82,7 @@ public interface SharedPreferences { /** * Set a set of String values in the preferences editor, to be written - * back once {@link #commit} is called. + * back once {@link #commit} or {@link #apply} is called. * * @param key The name of the preference to modify. * @param values The set of new values for the preference. Passing {@code null} @@ -355,7 +355,14 @@ public interface SharedPreferences { /** * Registers a callback to be invoked when a change happens to a preference. - * + * + * <p class="caution"><strong>Caution:</strong> The preference manager does + * not currently store a strong reference to the listener. You must store a + * strong reference to the listener, or it will be susceptible to garbage + * collection. We recommend you keep a reference to the listener in the + * instance data of an object that will exist as long as you need the + * listener.</p> + * * @param listener The callback that will run. * @see #unregisterOnSharedPreferenceChangeListener */ diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 0d1b262..6b44a11 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -453,7 +453,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { * * {@hide} */ - public String requiredCpuAbi; + public String cpuAbi; /** * The kernel user-ID that has been assigned to this application; @@ -592,7 +592,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { sourceDir = orig.sourceDir; publicSourceDir = orig.publicSourceDir; nativeLibraryDir = orig.nativeLibraryDir; - requiredCpuAbi = orig.requiredCpuAbi; + cpuAbi = orig.cpuAbi; resourceDirs = orig.resourceDirs; seinfo = orig.seinfo; sharedLibraryFiles = orig.sharedLibraryFiles; @@ -634,7 +634,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { dest.writeString(sourceDir); dest.writeString(publicSourceDir); dest.writeString(nativeLibraryDir); - dest.writeString(requiredCpuAbi); + dest.writeString(cpuAbi); dest.writeStringArray(resourceDirs); dest.writeString(seinfo); dest.writeStringArray(sharedLibraryFiles); @@ -675,7 +675,7 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable { sourceDir = source.readString(); publicSourceDir = source.readString(); nativeLibraryDir = source.readString(); - requiredCpuAbi = source.readString(); + cpuAbi = source.readString(); resourceDirs = source.readStringArray(); seinfo = source.readString(); sharedLibraryFiles = source.readStringArray(); diff --git a/core/java/android/content/pm/ContainerEncryptionParams.java b/core/java/android/content/pm/ContainerEncryptionParams.java index 88112a7..dd1332b 100644 --- a/core/java/android/content/pm/ContainerEncryptionParams.java +++ b/core/java/android/content/pm/ContainerEncryptionParams.java @@ -16,6 +16,7 @@ package android.content.pm; +import android.annotation.PrivateApi; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; @@ -31,8 +32,11 @@ import javax.crypto.spec.IvParameterSpec; /** * Represents encryption parameters used to read a container. * + * @deprecated encrypted containers are legacy. * @hide */ +@PrivateApi +@Deprecated public class ContainerEncryptionParams implements Parcelable { protected static final String TAG = "ContainerEncryptionParams"; diff --git a/core/java/android/content/pm/IPackageInstallObserver2.aidl b/core/java/android/content/pm/IPackageInstallObserver2.aidl index 2602ab5..7205ce7 100644 --- a/core/java/android/content/pm/IPackageInstallObserver2.aidl +++ b/core/java/android/content/pm/IPackageInstallObserver2.aidl @@ -1,22 +1,22 @@ /* -** -** 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. -*/ + * 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.IntentSender; import android.os.Bundle; /** @@ -40,6 +40,5 @@ oneway interface IPackageInstallObserver2 { * </tr> * </table> */ - void packageInstalled(in String packageName, in Bundle extras, int returnCode); + void packageInstalled(String basePackageName, in Bundle extras, int returnCode); } - diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl new file mode 100644 index 0000000..68c019b --- /dev/null +++ b/core/java/android/content/pm/IPackageInstaller.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 android.content.pm; + +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageInstallerSession; +import android.content.pm.PackageInstallerParams; +import android.os.ParcelFileDescriptor; + +/** {@hide} */ +interface IPackageInstaller { + int createSession(int userId, String installerPackageName, in PackageInstallerParams params); + IPackageInstallerSession openSession(int sessionId); + + int[] getSessions(int userId, String installerPackageName); + + void uninstall(int userId, String basePackageName, in IPackageDeleteObserver observer); + void uninstallSplit(int userId, String basePackageName, String splitName, in IPackageDeleteObserver observer); +} diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl new file mode 100644 index 0000000..f881acd --- /dev/null +++ b/core/java/android/content/pm/IPackageInstallerSession.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.content.pm.IPackageInstallObserver2; +import android.os.ParcelFileDescriptor; + +/** {@hide} */ +interface IPackageInstallerSession { + void updateProgress(int progress); + + ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes); + + void install(in IPackageInstallObserver2 observer); + void destroy(); +} diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 03eb50f..70668e1 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -26,6 +26,7 @@ import android.content.pm.ContainerEncryptionParams; import android.content.pm.FeatureInfo; import android.content.pm.IPackageInstallObserver; import android.content.pm.IPackageInstallObserver2; +import android.content.pm.IPackageInstaller; import android.content.pm.IPackageDeleteObserver; import android.content.pm.IPackageDataObserver; import android.content.pm.IPackageMoveObserver; @@ -111,7 +112,7 @@ interface IPackageManager { ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags, int userId); - boolean canForwardTo(in Intent intent, String resolvedType, int userIdFrom, int userIdDest); + boolean canForwardTo(in Intent intent, String resolvedType, int sourceUserId, int targetUserId); List<ResolveInfo> queryIntentActivities(in Intent intent, String resolvedType, int flags, int userId); @@ -247,10 +248,10 @@ interface IPackageManager { void clearPackagePersistentPreferredActivities(String packageName, int userId); - void addForwardingIntentFilter(in IntentFilter filter, boolean removable, int userIdOrig, - int userIdDest); + void addCrossProfileIntentFilter(in IntentFilter filter, boolean removable, int sourceUserId, + int targetUserId); - void clearForwardingIntentFilters(int userIdOrig); + void clearCrossProfileIntentFilters(int sourceUserId); /** * Report the set of 'Home' activity candidates, plus (if any) which of them @@ -432,6 +433,13 @@ interface IPackageManager { in VerificationParams verificationParams, in ContainerEncryptionParams encryptionParams); + void installPackageWithVerificationEncryptionAndAbiOverrideEtc(in Uri packageURI, + in IPackageInstallObserver observer, in IPackageInstallObserver2 observer2, + int flags, in String installerPackageName, + in VerificationParams verificationParams, + in ContainerEncryptionParams encryptionParams, + in String packageAbiOverride); + int installExistingPackageAsUser(String packageName, int userId); void verifyPendingInstall(int id, int verificationCode); @@ -450,4 +458,6 @@ interface IPackageManager { boolean setApplicationBlockedSettingAsUser(String packageName, boolean blocked, int userId); boolean getApplicationBlockedSettingAsUser(String packageName, int userId); + + IPackageInstaller getPackageInstaller(); } diff --git a/core/java/android/content/pm/LauncherActivityInfo.java b/core/java/android/content/pm/LauncherActivityInfo.java index 9087338..5d48868 100644 --- a/core/java/android/content/pm/LauncherActivityInfo.java +++ b/core/java/android/content/pm/LauncherActivityInfo.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; +import android.util.DisplayMetrics; import android.util.Log; /** @@ -47,21 +48,22 @@ public class LauncherActivityInfo { 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) { + LauncherActivityInfo(Context context, ResolveInfo info, UserHandle user, + long firstInstallTime) { this(context); - this.mActivityInfo = info.activityInfo; - this.mComponentName = LauncherApps.getComponentName(info); - this.mUser = user; + mActivityInfo = info.activityInfo; + mComponentName = LauncherApps.getComponentName(info); + mUser = user; + mFirstInstallTime = firstInstallTime; } LauncherActivityInfo(Context context) { @@ -79,7 +81,13 @@ public class LauncherActivityInfo { } /** - * Returns the user handle of the user profile that this activity belongs to. + * Returns the user handle of the user profile that this activity belongs to. In order to + * persist the identity of the profile, do not store the UserHandle. Instead retrieve its + * serial number from UserManager. You can convert the serial number back to a UserHandle + * for later use. + * + * @see UserManager#getSerialNumberForUser(UserHandle) + * @see UserManager#getUserForSerialNumber(long) * * @return The UserHandle of the profile. */ @@ -89,7 +97,7 @@ public class LauncherActivityInfo { /** * Retrieves the label for the activity. - * + * * @return The label for the activity. */ public CharSequence getLabel() { @@ -98,8 +106,10 @@ public class LauncherActivityInfo { /** * Returns the icon for this activity, without any badging for the profile. - * @param density The preferred density of the icon, zero for default density. + * @param density The preferred density of the icon, zero for default density. Use + * density DPI values from {@link DisplayMetrics}. * @see #getBadgedIcon(int) + * @see DisplayMetrics * @return The drawable associated with the activity */ public Drawable getIcon(int density) { @@ -109,15 +119,25 @@ public class LauncherActivityInfo { /** * Returns the application flags from the ApplicationInfo of the activity. - * + * * @return Application flags + * @hide remove before shipping */ public int getApplicationFlags() { return mActivityInfo.applicationInfo.flags; } /** + * Returns the application info for the appliction this activity belongs to. + * @return + */ + public ApplicationInfo getApplicationInfo() { + return mActivityInfo.applicationInfo; + } + + /** * Returns the time at which the package was first installed. + * * @return The time of installation of the package, in milliseconds. */ public long getFirstInstallTime() { @@ -134,7 +154,9 @@ public class LauncherActivityInfo { /** * Returns the activity icon with badging appropriate for the profile. - * @param density Optional density for the icon, or 0 to use the default density. + * @param density Optional density for the icon, or 0 to use the default density. Use + * {@link DisplayMetrics} for DPI values. + * @see DisplayMetrics * @return A badged icon for the activity. */ public Drawable getBadgedIcon(int density) { diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 8025b60..04c0b9f 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -16,15 +16,18 @@ package android.content.pm; +import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ILauncherApps; import android.content.pm.IOnAppsChangedListener; +import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Rect; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; import android.util.Log; import java.util.ArrayList; @@ -36,6 +39,12 @@ import java.util.List; * 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. + * <p> + * To watch for managed profiles being added or removed, register for the following broadcasts: + * {@link Intent#ACTION_MANAGED_PROFILE_ADDED} and {@link Intent#ACTION_MANAGED_PROFILE_REMOVED}. + * <p> + * You can retrieve the list of profiles associated with this user with + * {@link UserManager#getUserProfiles()}. */ public class LauncherApps { @@ -44,12 +53,13 @@ public class LauncherApps { private Context mContext; private ILauncherApps mService; + private PackageManager mPm; private List<OnAppsChangedListener> mListeners = new ArrayList<OnAppsChangedListener>(); /** - * Callbacks for changes to this and related managed profiles. + * Callbacks for package changes to this and related managed profiles. */ public interface OnAppsChangedListener { /** @@ -57,6 +67,7 @@ public class LauncherApps { * * @param user The UserHandle of the profile that generated the change. * @param packageName The name of the package that was removed. + * @hide remove before ship */ void onPackageRemoved(UserHandle user, String packageName); @@ -65,6 +76,7 @@ public class LauncherApps { * * @param user The UserHandle of the profile that generated the change. * @param packageName The name of the package that was added. + * @hide remove before ship */ void onPackageAdded(UserHandle user, String packageName); @@ -73,6 +85,7 @@ public class LauncherApps { * * @param user The UserHandle of the profile that generated the change. * @param packageName The name of the package that has changed. + * @hide remove before ship */ void onPackageChanged(UserHandle user, String packageName); @@ -86,6 +99,7 @@ public class LauncherApps { * available. * @param replacing Indicates whether these packages are replacing * existing ones. + * @hide remove before ship */ void onPackagesAvailable(UserHandle user, String[] packageNames, boolean replacing); @@ -99,14 +113,66 @@ public class LauncherApps { * unavailable. * @param replacing Indicates whether the packages are about to be * replaced with new versions. + * @hide remove before ship */ void onPackagesUnavailable(UserHandle user, String[] packageNames, boolean replacing); + + /** + * Indicates that a package was removed from the specified profile. + * + * @param packageName The name of the package that was removed. + * @param user The UserHandle of the profile that generated the change. + */ + void onPackageRemoved(String packageName, UserHandle user); + + /** + * Indicates that a package was added to the specified profile. + * + * @param packageName The name of the package that was added. + * @param user The UserHandle of the profile that generated the change. + */ + void onPackageAdded(String packageName, UserHandle user); + + /** + * Indicates that a package was modified in the specified profile. + * + * @param packageName The name of the package that has changed. + * @param user The UserHandle of the profile that generated the change. + */ + void onPackageChanged(String packageName, UserHandle user); + + /** + * Indicates that one or more packages have become available. For + * example, this can happen when a removable storage card has + * reappeared. + * + * @param packageNames The names of the packages that have become + * available. + * @param user The UserHandle of the profile that generated the change. + * @param replacing Indicates whether these packages are replacing + * existing ones. + */ + void onPackagesAvailable(String [] packageNames, UserHandle user, 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 packageNames The names of the packages that have become + * unavailable. + * @param user The UserHandle of the profile that generated the change. + * @param replacing Indicates whether the packages are about to be + * replaced with new versions. + */ + void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing); } /** @hide */ public LauncherApps(Context context, ILauncherApps service) { mContext = context; mService = service; + mPm = context.getPackageManager(); } /** @@ -131,7 +197,15 @@ public class LauncherApps { final int count = activities.size(); for (int i = 0; i < count; i++) { ResolveInfo ri = activities.get(i); - LauncherActivityInfo lai = new LauncherActivityInfo(mContext, ri, user); + long firstInstallTime = 0; + try { + firstInstallTime = mPm.getPackageInfo(ri.activityInfo.packageName, + PackageManager.GET_UNINSTALLED_PACKAGES).firstInstallTime; + } catch (NameNotFoundException nnfe) { + // Sorry, can't find package + } + LauncherActivityInfo lai = new LauncherActivityInfo(mContext, ri, user, + firstInstallTime); if (DEBUG) { Log.v(TAG, "Returning activity for profile " + user + " : " + lai.getComponentName()); @@ -157,7 +231,15 @@ public class LauncherApps { try { ResolveInfo ri = mService.resolveActivity(intent, user); if (ri != null) { - LauncherActivityInfo info = new LauncherActivityInfo(mContext, ri, user); + long firstInstallTime = 0; + try { + firstInstallTime = mPm.getPackageInfo(ri.activityInfo.packageName, + PackageManager.GET_UNINSTALLED_PACKAGES).firstInstallTime; + } catch (NameNotFoundException nnfe) { + // Sorry, can't find package + } + LauncherActivityInfo info = new LauncherActivityInfo(mContext, ri, user, + firstInstallTime); return info; } } catch (RemoteException re) { @@ -173,9 +255,23 @@ public class LauncherApps { * @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 + * @hide remove before ship */ public void startActivityForProfile(ComponentName component, Rect sourceBounds, Bundle opts, UserHandle user) { + startActivityForProfile(component, user, sourceBounds, opts); + } + + /** + * Starts an activity in the specified profile. + * + * @param component The ComponentName of the activity to launch + * @param user The UserHandle of the profile + * @param sourceBounds The Rect containing the source bounds of the clicked icon + * @param opts Options to pass to startActivity + */ + public void startActivityForProfile(ComponentName component, UserHandle user, Rect sourceBounds, + Bundle opts) { if (DEBUG) { Log.i(TAG, "StartActivityForProfile " + component + " " + user.getIdentifier()); } @@ -224,13 +320,15 @@ public class LauncherApps { * * @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) { + public void addOnAppsChangedListener(OnAppsChangedListener listener) { + synchronized (this) { + if (listener != null && !mListeners.contains(listener)) { + mListeners.add(listener); + if (mListeners.size() == 1) { + try { + mService.addOnAppsChangedListener(mAppsChangedListener); + } catch (RemoteException re) { + } } } } @@ -242,12 +340,14 @@ public class LauncherApps { * @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) { + public void removeOnAppsChangedListener(OnAppsChangedListener listener) { + synchronized (this) { + mListeners.remove(listener); + if (mListeners.size() == 0) { + try { + mService.removeOnAppsChangedListener(mAppsChangedListener); + } catch (RemoteException re) { + } } } } @@ -261,7 +361,8 @@ public class LauncherApps { } synchronized (LauncherApps.this) { for (OnAppsChangedListener listener : mListeners) { - listener.onPackageRemoved(user, packageName); + listener.onPackageRemoved(user, packageName); // TODO: Remove before ship + listener.onPackageRemoved(packageName, user); } } } @@ -273,7 +374,8 @@ public class LauncherApps { } synchronized (LauncherApps.this) { for (OnAppsChangedListener listener : mListeners) { - listener.onPackageChanged(user, packageName); + listener.onPackageChanged(user, packageName); // TODO: Remove before ship + listener.onPackageChanged(packageName, user); } } } @@ -285,7 +387,8 @@ public class LauncherApps { } synchronized (LauncherApps.this) { for (OnAppsChangedListener listener : mListeners) { - listener.onPackageAdded(user, packageName); + listener.onPackageAdded(user, packageName); // TODO: Remove before ship + listener.onPackageAdded(packageName, user); } } } @@ -298,7 +401,8 @@ public class LauncherApps { } synchronized (LauncherApps.this) { for (OnAppsChangedListener listener : mListeners) { - listener.onPackagesAvailable(user, packageNames, replacing); + listener.onPackagesAvailable(user, packageNames, replacing); // TODO: Remove + listener.onPackagesAvailable(packageNames, user, replacing); } } } @@ -311,7 +415,8 @@ public class LauncherApps { } synchronized (LauncherApps.this) { for (OnAppsChangedListener listener : mListeners) { - listener.onPackagesUnavailable(user, packageNames, replacing); + listener.onPackagesUnavailable(user, packageNames, replacing); // TODO: Remove + listener.onPackagesUnavailable(packageNames, user, replacing); } } } diff --git a/core/java/android/content/pm/ManifestDigest.java b/core/java/android/content/pm/ManifestDigest.java index 409b5ae..943534f 100644 --- a/core/java/android/content/pm/ManifestDigest.java +++ b/core/java/android/content/pm/ManifestDigest.java @@ -16,6 +16,7 @@ package android.content.pm; +import android.annotation.PrivateApi; import android.os.Parcel; import android.os.Parcelable; import android.util.Slog; @@ -36,6 +37,7 @@ import libcore.io.IoUtils; * * @hide */ +@PrivateApi public class ManifestDigest implements Parcelable { private static final String TAG = "ManifestDigest"; diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java new file mode 100644 index 0000000..4672015 --- /dev/null +++ b/core/java/android/content/pm/PackageInstaller.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.content.pm; + +import android.app.PackageInstallObserver; +import android.app.PackageUninstallObserver; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.FileBridge; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; + +import java.io.OutputStream; + +/** {@hide} */ +public class PackageInstaller { + private final PackageManager mPm; + private final IPackageInstaller mInstaller; + private final int mUserId; + private final String mInstallerPackageName; + + /** {@hide} */ + public PackageInstaller(PackageManager pm, IPackageInstaller installer, int userId, + String installerPackageName) { + mPm = pm; + mInstaller = installer; + mUserId = userId; + mInstallerPackageName = installerPackageName; + } + + public boolean isPackageAvailable(String basePackageName) { + try { + final ApplicationInfo info = mPm.getApplicationInfo(basePackageName, + PackageManager.GET_UNINSTALLED_PACKAGES); + return ((info.flags & ApplicationInfo.FLAG_INSTALLED) != 0); + } catch (NameNotFoundException e) { + return false; + } + } + + public void installAvailablePackage(String basePackageName, PackageInstallObserver observer) { + int returnCode; + try { + returnCode = mPm.installExistingPackage(basePackageName); + } catch (NameNotFoundException e) { + returnCode = PackageManager.INSTALL_FAILED_PACKAGE_CHANGED; + } + observer.packageInstalled(basePackageName, null, returnCode); + } + + public int createSession(PackageInstallerParams params) { + try { + return mInstaller.createSession(mUserId, mInstallerPackageName, params); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public Session openSession(int sessionId) { + try { + return new Session(mInstaller.openSession(sessionId)); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public int[] getSessions() { + try { + return mInstaller.getSessions(mUserId, mInstallerPackageName); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public void uninstall(String basePackageName, PackageUninstallObserver observer) { + try { + mInstaller.uninstall(mUserId, basePackageName, observer.getBinder()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public void uninstall(String basePackageName, String splitName, + PackageUninstallObserver observer) { + try { + mInstaller.uninstallSplit(mUserId, basePackageName, splitName, observer.getBinder()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + /** + * An installation that is being actively staged. For an install to succeed, + * all existing and new packages must have identical package names, version + * codes, and signing certificates. + * <p> + * A session may contain any number of split packages. If the application + * does not yet exist, this session must include a base package. + * <p> + * If a package included in this session is already defined by the existing + * installation (for example, the same split name), the package in this + * session will replace the existing package. + */ + public class Session { + private IPackageInstallerSession mSession; + + /** {@hide} */ + public Session(IPackageInstallerSession session) { + mSession = session; + } + + public void updateProgress(int progress) { + try { + mSession.updateProgress(progress); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + /** + * Open an APK file for writing, starting at the given offset. You can + * then stream data into the file, periodically calling + * {@link OutputStream#flush()} to ensure bytes have been written to + * disk. + */ + public OutputStream openWrite(String splitName, long offsetBytes, long lengthBytes) { + try { + final ParcelFileDescriptor clientSocket = mSession.openWrite(splitName, + offsetBytes, lengthBytes); + return new FileBridge.FileBridgeOutputStream(clientSocket.getFileDescriptor()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public void install(PackageInstallObserver observer) { + try { + mSession.install(observer.getBinder()); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + public void close() { + // No resources to release at the moment + } + + public void destroy() { + try { + mSession.destroy(); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + } +} diff --git a/core/java/android/content/pm/PackageInstallerParams.aidl b/core/java/android/content/pm/PackageInstallerParams.aidl new file mode 100644 index 0000000..b3dde21 --- /dev/null +++ b/core/java/android/content/pm/PackageInstallerParams.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.content.pm; + +parcelable PackageInstallerParams; diff --git a/core/java/android/content/pm/PackageInstallerParams.java b/core/java/android/content/pm/PackageInstallerParams.java new file mode 100644 index 0000000..67cf276 --- /dev/null +++ b/core/java/android/content/pm/PackageInstallerParams.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.content.pm; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Parameters that define an installation session. + * + * {@hide} + */ +public class PackageInstallerParams implements Parcelable { + + // TODO: extend to support remaining VerificationParams + + /** {@hide} */ + public boolean fullInstall; + /** {@hide} */ + public int installFlags; + /** {@hide} */ + public int installLocation = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY; + /** {@hide} */ + public Signature[] signatures; + /** {@hide} */ + public long deltaSize = -1; + /** {@hide} */ + public Bitmap icon; + /** {@hide} */ + public String title; + /** {@hide} */ + public Uri originatingUri; + /** {@hide} */ + public Uri referrerUri; + + public PackageInstallerParams() { + } + + /** {@hide} */ + public PackageInstallerParams(Parcel source) { + this.fullInstall = source.readInt() != 0; + this.installFlags = source.readInt(); + this.installLocation = source.readInt(); + this.signatures = (Signature[]) source.readParcelableArray(null); + deltaSize = source.readLong(); + if (source.readInt() != 0) { + icon = Bitmap.CREATOR.createFromParcel(source); + } + title = source.readString(); + originatingUri = Uri.CREATOR.createFromParcel(source); + referrerUri = Uri.CREATOR.createFromParcel(source); + } + + public void setFullInstall(boolean fullInstall) { + this.fullInstall = fullInstall; + } + + public void setInstallFlags(int installFlags) { + this.installFlags = installFlags; + } + + public void setInstallLocation(int installLocation) { + this.installLocation = installLocation; + } + + public void setSignatures(Signature[] signatures) { + this.signatures = signatures; + } + + public void setDeltaSize(long deltaSize) { + this.deltaSize = deltaSize; + } + + public void setIcon(Bitmap icon) { + this.icon = icon; + } + + public void setTitle(CharSequence title) { + this.title = (title != null) ? title.toString() : null; + } + + public void setOriginatingUri(Uri originatingUri) { + this.originatingUri = originatingUri; + } + + public void setReferrerUri(Uri referrerUri) { + this.referrerUri = referrerUri; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(fullInstall ? 1 : 0); + dest.writeInt(installFlags); + dest.writeInt(installLocation); + dest.writeParcelableArray(signatures, flags); + dest.writeLong(deltaSize); + if (icon != null) { + dest.writeInt(1); + icon.writeToParcel(dest, flags); + } else { + dest.writeInt(0); + } + dest.writeString(title); + dest.writeParcelable(originatingUri, flags); + dest.writeParcelable(referrerUri, flags); + } + + public static final Parcelable.Creator<PackageInstallerParams> + CREATOR = new Parcelable.Creator<PackageInstallerParams>() { + @Override + public PackageInstallerParams createFromParcel(Parcel p) { + return new PackageInstallerParams(p); + } + + @Override + public PackageInstallerParams[] newArray(int size) { + return new PackageInstallerParams[size]; + } + }; +} diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index eb2c11f..c5cd5c9 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -17,6 +17,7 @@ package android.content.pm; import android.annotation.IntDef; +import android.annotation.PrivateApi; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.app.PackageInstallObserver; @@ -369,6 +370,7 @@ public abstract class PackageManager { * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} on success. * @hide */ + @PrivateApi public static final int INSTALL_SUCCEEDED = 1; /** @@ -377,6 +379,7 @@ public abstract class PackageManager { * already installed. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_ALREADY_EXISTS = -1; /** @@ -385,6 +388,7 @@ public abstract class PackageManager { * file is invalid. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_INVALID_APK = -2; /** @@ -393,6 +397,7 @@ public abstract class PackageManager { * is invalid. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_INVALID_URI = -3; /** @@ -401,6 +406,7 @@ public abstract class PackageManager { * service found that the device didn't have enough storage space to install the app. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_INSUFFICIENT_STORAGE = -4; /** @@ -409,6 +415,7 @@ public abstract class PackageManager { * package is already installed with the same name. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_DUPLICATE_PACKAGE = -5; /** @@ -417,6 +424,7 @@ public abstract class PackageManager { * the requested shared user does not exist. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_NO_SHARED_USER = -6; /** @@ -426,6 +434,7 @@ public abstract class PackageManager { * than the new package (and the old package's data was not removed). * @hide */ + @PrivateApi public static final int INSTALL_FAILED_UPDATE_INCOMPATIBLE = -7; /** @@ -435,6 +444,7 @@ public abstract class PackageManager { * device and does not have matching signature. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_SHARED_USER_INCOMPATIBLE = -8; /** @@ -443,6 +453,7 @@ public abstract class PackageManager { * the new package uses a shared library that is not available. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_MISSING_SHARED_LIBRARY = -9; /** @@ -451,6 +462,7 @@ public abstract class PackageManager { * the new package uses a shared library that is not available. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_REPLACE_COULDNT_DELETE = -10; /** @@ -460,6 +472,7 @@ public abstract class PackageManager { * either because there was not enough storage or the validation failed. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_DEXOPT = -11; /** @@ -469,6 +482,7 @@ public abstract class PackageManager { * that required by the package. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_OLDER_SDK = -12; /** @@ -478,6 +492,7 @@ public abstract class PackageManager { * same authority as a provider already installed in the system. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_CONFLICTING_PROVIDER = -13; /** @@ -487,6 +502,7 @@ public abstract class PackageManager { * that required by the package. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_NEWER_SDK = -14; /** @@ -497,6 +513,7 @@ public abstract class PackageManager { * flag. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_TEST_ONLY = -15; /** @@ -506,6 +523,7 @@ public abstract class PackageManager { * compatible with the the device's CPU_ABI. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_CPU_ABI_INCOMPATIBLE = -16; /** @@ -514,6 +532,7 @@ public abstract class PackageManager { * the new package uses a feature that is not available. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_MISSING_FEATURE = -17; // ------ Errors related to sdcard @@ -523,6 +542,7 @@ public abstract class PackageManager { * a secure container mount point couldn't be accessed on external media. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_CONTAINER_ERROR = -18; /** @@ -532,6 +552,7 @@ public abstract class PackageManager { * location. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_INVALID_INSTALL_LOCATION = -19; /** @@ -541,6 +562,7 @@ public abstract class PackageManager { * location because the media is not available. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_MEDIA_UNAVAILABLE = -20; /** @@ -549,6 +571,7 @@ public abstract class PackageManager { * the new package couldn't be installed because the verification timed out. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_VERIFICATION_TIMEOUT = -21; /** @@ -557,6 +580,7 @@ public abstract class PackageManager { * the new package couldn't be installed because the verification did not succeed. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_VERIFICATION_FAILURE = -22; /** @@ -565,6 +589,7 @@ public abstract class PackageManager { * the package changed from what the calling program expected. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_PACKAGE_CHANGED = -23; /** @@ -590,6 +615,7 @@ public abstract class PackageManager { * '.apk' extension. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_NOT_APK = -100; /** @@ -598,6 +624,7 @@ public abstract class PackageManager { * if the parser was unable to retrieve the AndroidManifest.xml file. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_BAD_MANIFEST = -101; /** @@ -606,6 +633,7 @@ public abstract class PackageManager { * if the parser encountered an unexpected exception. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION = -102; /** @@ -614,6 +642,7 @@ public abstract class PackageManager { * if the parser did not find any certificates in the .apk. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_NO_CERTIFICATES = -103; /** @@ -622,6 +651,7 @@ public abstract class PackageManager { * if the parser found inconsistent certificates on the files in the .apk. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES = -104; /** @@ -631,6 +661,7 @@ public abstract class PackageManager { * files in the .apk. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING = -105; /** @@ -639,6 +670,7 @@ public abstract class PackageManager { * if the parser encountered a bad or missing package name in the manifest. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME = -106; /** @@ -647,6 +679,7 @@ public abstract class PackageManager { * if the parser encountered a bad shared user id name in the manifest. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID = -107; /** @@ -655,6 +688,7 @@ public abstract class PackageManager { * if the parser encountered some structural problem in the manifest. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_MANIFEST_MALFORMED = -108; /** @@ -664,6 +698,7 @@ public abstract class PackageManager { * in the manifest. * @hide */ + @PrivateApi public static final int INSTALL_PARSE_FAILED_MANIFEST_EMPTY = -109; /** @@ -672,6 +707,7 @@ public abstract class PackageManager { * if the system failed to install the package because of system issues. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_INTERNAL_ERROR = -110; /** @@ -1369,6 +1405,14 @@ public abstract class PackageManager { public static final String FEATURE_MANAGEDPROFILES = "android.software.managedprofiles"; /** + * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: + * The device has a full implementation of the android.webkit.* APIs. Devices + * lacking this feature will not have a functioning WebView implementation. + */ + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_WEBVIEW = "android.software.webview"; + + /** * Action to external storage service to clean out removed apps. * @hide */ @@ -2863,6 +2907,7 @@ public abstract class PackageManager { * instead. This method will continue to be supported but the older observer interface * will not get additional failure details. */ + // @PrivateApi public abstract void installPackage( Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName); @@ -2897,6 +2942,7 @@ public abstract class PackageManager { * continue to be supported but the older observer interface will not get additional failure * details. */ + // @PrivateApi public abstract void installPackageWithVerification(Uri packageURI, IPackageInstallObserver observer, int flags, String installerPackageName, Uri verificationURI, ManifestDigest manifestDigest, @@ -3025,6 +3071,7 @@ public abstract class PackageManager { * on the system for other users, also install it for the calling user. * @hide */ + // @PrivateApi public abstract int installExistingPackage(String packageName) throws NameNotFoundException; @@ -3114,6 +3161,7 @@ public abstract class PackageManager { * * @hide */ + // @PrivateApi public abstract void deletePackage( String packageName, IPackageDeleteObserver observer, int flags); @@ -3182,6 +3230,7 @@ public abstract class PackageManager { * * @hide */ + // @PrivateApi public abstract void freeStorageAndNotify(long freeStorageSize, IPackageDataObserver observer); /** @@ -3510,6 +3559,9 @@ public abstract class PackageManager { */ public abstract VerifierDeviceIdentity getVerifierDeviceIdentity(); + /** {@hide} */ + public abstract PackageInstaller getPackageInstaller(); + /** * Returns the data directory for a particular user and package, given the uid of the package. * @param uid uid of the package, including the userId and appId @@ -3524,24 +3576,38 @@ public abstract class PackageManager { } /** - * Adds a forwarding intent filter. After calling this method all intents sent from the user - * with id userIdOrig can also be be resolved by activities in the user with id userIdDest if - * they match the specified intent filter. - * @param filter the {@link IntentFilter} the intent has to match to be forwarded - * @param removable if set to false, {@link clearForwardingIntents} will not remove this intent - * filter - * @param userIdOrig user from which the intent can be forwarded - * @param userIdDest user to which the intent can be forwarded + * Adds a {@link CrossProfileIntentFilter}. After calling this method all intents sent from the + * user with id sourceUserId can also be be resolved by activities in the user with id + * targetUserId if they match the specified intent filter. + * @param filter the {@link IntentFilter} the intent has to match + * @param removable if set to false, {@link clearCrossProfileIntentFilters} will not remove this + * {@link CrossProfileIntentFilter} * @hide */ + public abstract void addCrossProfileIntentFilter(IntentFilter filter, boolean removable, + int sourceUserId, int targetUserId); + + /** + * @hide + * @deprecated + * TODO: remove it as soon as the code of ManagedProvisionning is updated + */ public abstract void addForwardingIntentFilter(IntentFilter filter, boolean removable, - int userIdOrig, int userIdDest); + int sourceUserId, int targetUserId); /** - * Clearing all removable {@link ForwardingIntentFilter}s that are set with the given user as - * the origin. - * @param userIdOrig user from which the intent can be forwarded + * Clearing removable {@link CrossProfileIntentFilter}s which have the specified user as their + * source + * @param sourceUserId + * be cleared. * @hide */ - public abstract void clearForwardingIntentFilters(int userIdOrig); + public abstract void clearCrossProfileIntentFilters(int sourceUserId); + + /** + * @hide + * @deprecated + * TODO: remove it as soon as the code of ManagedProvisionning is updated + */ + public abstract void clearForwardingIntentFilters(int sourceUserId); } diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java index ff96c51..ab8bf61 100644 --- a/core/java/android/content/pm/PackageParser.java +++ b/core/java/android/content/pm/PackageParser.java @@ -16,6 +16,9 @@ package android.content.pm; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; +import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; + import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -32,6 +35,7 @@ import android.util.AttributeSet; import android.util.Base64; import android.util.DisplayMetrics; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.util.TypedValue; @@ -41,6 +45,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.lang.ref.WeakReference; +import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.NoSuchAlgorithmException; import java.security.PublicKey; @@ -51,7 +56,6 @@ 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; import java.util.Iterator; @@ -61,6 +65,7 @@ import java.util.Set; import java.util.jar.StrictJarFile; import java.util.zip.ZipEntry; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -207,16 +212,20 @@ public class PackageParser { */ public static class PackageLite { public final String packageName; + public final String splitName; public final int versionCode; public final int installLocation; public final VerifierInfo[] verifiers; + public final Signature[] signatures; - public PackageLite(String packageName, int versionCode, - int installLocation, List<VerifierInfo> verifiers) { + public PackageLite(String packageName, String splitName, int versionCode, + int installLocation, List<VerifierInfo> verifiers, Signature[] signatures) { this.packageName = packageName; + this.splitName = splitName; this.versionCode = versionCode; this.installLocation = installLocation; this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]); + this.signatures = signatures; } } @@ -459,7 +468,7 @@ public class PackageParser { return pi; } - private Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry je, + private static Certificate[][] loadCertificates(StrictJarFile jarFile, ZipEntry je, byte[] readBuffer) { try { // We must read the stream for the JarEntry to retrieve @@ -486,6 +495,7 @@ public class PackageParser { public final static int PARSE_ON_SDCARD = 1<<5; public final static int PARSE_IS_SYSTEM_DIR = 1<<6; public final static int PARSE_IS_PRIVILEGED = 1<<7; + public final static int PARSE_GET_SIGNATURES = 1<<8; public int getParseError() { return mParseError; @@ -722,12 +732,8 @@ public class PackageParser { mReadBuffer = readBufferRef; } - if (certs != null && certs.length > 0) { - 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]); - } + if (!ArrayUtils.isEmpty(certs)) { + pkg.mSignatures = convertToSignatures(certs); } else { Slog.e(TAG, "Package " + pkg.packageName + " has no certificates; ignoring!"); @@ -762,6 +768,39 @@ public class PackageParser { return true; } + /** + * Only collect certificates on the manifest; does not validate signatures + * across remainder of package. + */ + private static Signature[] collectCertificates(String packageFilePath) { + try { + final StrictJarFile jarFile = new StrictJarFile(packageFilePath); + try { + final ZipEntry jarEntry = jarFile.findEntry(ANDROID_MANIFEST_FILENAME); + if (jarEntry != null) { + final Certificate[][] certs = loadCertificates(jarFile, jarEntry, null); + return convertToSignatures(certs); + } + } finally { + jarFile.close(); + } + } catch (GeneralSecurityException e) { + Slog.w(TAG, "Failed to collect certs from " + packageFilePath + ": " + e); + } catch (IOException e) { + Slog.w(TAG, "Failed to collect certs from " + packageFilePath + ": " + e); + } + return null; + } + + private static Signature[] convertToSignatures(Certificate[][] certs) + throws CertificateEncodingException { + final Signature[] res = new Signature[certs.length]; + for (int i = 0; i < certs.length; i++) { + res[i] = new Signature(certs[i]); + } + return res; + } + /* * Utility method that retrieves just the package name and install * location from the apk location at the given file path. @@ -794,11 +833,22 @@ public class PackageParser { return null; } + // Only collect certificates on the manifest; does not validate + // signatures across remainder of package. + final Signature[] signatures; + if ((flags & PARSE_GET_SIGNATURES) != 0) { + signatures = collectCertificates(packageFilePath); + } else { + signatures = null; + } + final AttributeSet attrs = parser; final String errors[] = new String[1]; PackageLite packageLite = null; try { - packageLite = parsePackageLite(res, parser, attrs, flags, errors); + packageLite = parsePackageLite(res, parser, attrs, flags, signatures, errors); + } catch (PackageParserException e) { + Slog.w(TAG, packageFilePath, e); } catch (IOException e) { Slog.w(TAG, packageFilePath, e); } catch (XmlPullParserException e) { @@ -840,72 +890,51 @@ public class PackageParser { ? null : "must have at least one '.' separator"; } - private static String parsePackageName(XmlPullParser parser, - AttributeSet attrs, int flags, String[] outError) - throws IOException, XmlPullParserException { + private static Pair<String, String> parsePackageSplitNames(XmlPullParser parser, + AttributeSet attrs, int flags) throws IOException, XmlPullParserException, + PackageParserException { int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { - ; } if (type != XmlPullParser.START_TAG) { - outError[0] = "No start tag found"; - return null; + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "No start tag found"); } - if (DEBUG_PARSER) - Slog.v(TAG, "Root element name: '" + parser.getName() + "'"); if (!parser.getName().equals("manifest")) { - outError[0] = "No <manifest> tag"; - return null; + throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, + "No <manifest> tag"); } - String pkgName = attrs.getAttributeValue(null, "package"); - if (pkgName == null || pkgName.length() == 0) { - outError[0] = "<manifest> does not specify package"; - return null; + + final String packageName = attrs.getAttributeValue(null, "package"); + if (!"android".equals(packageName)) { + final String error = validateName(packageName, true); + if (error != null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, + "Invalid manifest package: " + error); + } } - String nameError = validateName(pkgName, true); - if (nameError != null && !"android".equals(pkgName)) { - outError[0] = "<manifest> specifies bad package name \"" - + pkgName + "\": " + nameError; - return null; + + final String splitName = attrs.getAttributeValue(null, "split"); + if (splitName != null) { + final String error = validateName(splitName, true); + if (error != null) { + throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, + "Invalid manifest split: " + error); + } } - return pkgName.intern(); + return Pair.create(packageName.intern(), + (splitName != null) ? splitName.intern() : splitName); } private static PackageLite parsePackageLite(Resources res, XmlPullParser parser, - AttributeSet attrs, int flags, String[] outError) throws IOException, - XmlPullParserException { + AttributeSet attrs, int flags, Signature[] signatures, String[] outError) + throws IOException, XmlPullParserException, PackageParserException { + final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs, flags); - int type; - while ((type = parser.next()) != XmlPullParser.START_TAG - && type != XmlPullParser.END_DOCUMENT) { - ; - } - - if (type != XmlPullParser.START_TAG) { - outError[0] = "No start tag found"; - return null; - } - if (DEBUG_PARSER) - Slog.v(TAG, "Root element name: '" + parser.getName() + "'"); - if (!parser.getName().equals("manifest")) { - outError[0] = "No <manifest> tag"; - return null; - } - String pkgName = attrs.getAttributeValue(null, "package"); - if (pkgName == null || pkgName.length() == 0) { - outError[0] = "<manifest> does not specify package"; - return null; - } - String nameError = validateName(pkgName, true); - if (nameError != null && !"android".equals(pkgName)) { - outError[0] = "<manifest> specifies bad package name \"" - + pkgName + "\": " + nameError; - return null; - } int installLocation = PARSE_DEFAULT_INSTALL_LOCATION; int versionCode = 0; int numFound = 0; @@ -925,6 +954,7 @@ public class PackageParser { } // Only search the tree when the tag is directly below <manifest> + int type; final int searchDepth = parser.getDepth() + 1; final List<VerifierInfo> verifiers = new ArrayList<VerifierInfo>(); @@ -942,7 +972,8 @@ public class PackageParser { } } - return new PackageLite(pkgName.intern(), versionCode, installLocation, verifiers); + return new PackageLite(packageSplit.first, packageSplit.second, versionCode, + installLocation, verifiers, signatures); } /** @@ -966,12 +997,18 @@ public class PackageParser { mParseActivityArgs = null; mParseServiceArgs = null; mParseProviderArgs = null; - - String pkgName = parsePackageName(parser, attrs, flags, outError); - if (pkgName == null) { + + final String pkgName; + final String splitName; + try { + Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs, flags); + pkgName = packageSplit.first; + splitName = packageSplit.second; + } catch (PackageParserException e) { mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; return null; } + int type; if (mOnlyCoreApps) { @@ -982,9 +1019,9 @@ public class PackageParser { } } - final Package pkg = new Package(pkgName); + final Package pkg = new Package(pkgName, splitName); boolean foundApp = false; - + TypedArray sa = res.obtainAttributes(attrs, com.android.internal.R.styleable.AndroidManifest); pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger( @@ -2137,7 +2174,6 @@ public class PackageParser { } final int innerDepth = parser.getDepth(); - int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { @@ -2511,13 +2547,13 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestActivity_singleUser, false)) { a.info.flags |= ActivityInfo.FLAG_SINGLE_USER; - if (a.info.exported) { + if (a.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { Slog.w(TAG, "Activity exported request ignored due to singleUser: " + a.className + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); a.info.exported = false; + setExported = true; } - setExported = true; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestActivity_primaryUserOnly, @@ -2870,7 +2906,7 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestProvider_singleUser, false)) { p.info.flags |= ProviderInfo.FLAG_SINGLE_USER; - if (p.info.exported) { + if (p.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { Slog.w(TAG, "Provider exported request ignored due to singleUser: " + p.className + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); @@ -3144,13 +3180,13 @@ public class PackageParser { com.android.internal.R.styleable.AndroidManifestService_singleUser, false)) { s.info.flags |= ServiceInfo.FLAG_SINGLE_USER; - if (s.info.exported) { + if (s.info.exported && (flags & PARSE_IS_PRIVILEGED) == 0) { Slog.w(TAG, "Service exported request ignored due to singleUser: " + s.className + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); s.info.exported = false; + setExported = true; } - setExported = true; } sa.recycle(); @@ -3544,6 +3580,7 @@ public class PackageParser { public final static class Package { public String packageName; + public String splitName; // For now we only support one application per package. public final ApplicationInfo applicationInfo = new ApplicationInfo(); @@ -3660,9 +3697,10 @@ public class PackageParser { public Set<PublicKey> mSigningKeys; public Map<String, Set<PublicKey>> mKeySetMapping; - public Package(String _name) { - packageName = _name; - applicationInfo.packageName = _name; + public Package(String packageName, String splitName) { + this.packageName = packageName; + this.splitName = splitName; + applicationInfo.packageName = packageName; applicationInfo.uid = -1; } @@ -4267,4 +4305,13 @@ public class PackageParser { public static void setCompatibilityModeEnabled(boolean compatibilityModeEnabled) { sCompatibilityModeEnabled = compatibilityModeEnabled; } + + public static class PackageParserException extends Exception { + public final int error; + + public PackageParserException(int error, String detailMessage) { + super(detailMessage); + this.error = error; + } + } } diff --git a/core/java/android/content/pm/Signature.java b/core/java/android/content/pm/Signature.java index f4e7dc3..96aa083 100644 --- a/core/java/android/content/pm/Signature.java +++ b/core/java/android/content/pm/Signature.java @@ -31,8 +31,10 @@ import java.security.cert.CertificateFactory; import java.util.Arrays; /** - * Opaque, immutable representation of a signature associated with an + * Opaque, immutable representation of a signing certificate associated with an * application package. + * <p> + * This class name is slightly misleading, since it's not actually a signature. */ public class Signature implements Parcelable { private final byte[] mSignature; diff --git a/core/java/android/content/pm/VerificationParams.java b/core/java/android/content/pm/VerificationParams.java index 22e1a85..bf1f77f 100644 --- a/core/java/android/content/pm/VerificationParams.java +++ b/core/java/android/content/pm/VerificationParams.java @@ -24,8 +24,10 @@ import android.os.Parcelable; /** * Represents verification parameters used to verify packages to be installed. * + * @deprecated callers should migrate to {@link PackageInstallerParams}. * @hide */ +@Deprecated public class VerificationParams implements Parcelable { /** A constant used to indicate that a uid value is not present. */ public static final int NO_UID = -1; diff --git a/core/java/android/content/res/ColorStateList.java b/core/java/android/content/res/ColorStateList.java index 5674154..3f01dd2 100644 --- a/core/java/android/content/res/ColorStateList.java +++ b/core/java/android/content/res/ColorStateList.java @@ -64,7 +64,6 @@ import java.util.Arrays; * List Resource</a>.</p> */ public class ColorStateList implements Parcelable { - private int[][] mStateSpecs; // must be parallel to mColors private int[] mColors; // must be parallel to mStateSpecs private int mDefaultColor = 0xffff0000; @@ -100,9 +99,9 @@ public class ColorStateList implements Parcelable { public static ColorStateList valueOf(int color) { // TODO: should we collect these eventually? synchronized (sCache) { - WeakReference<ColorStateList> ref = sCache.get(color); - ColorStateList csl = ref != null ? ref.get() : null; + final WeakReference<ColorStateList> ref = sCache.get(color); + ColorStateList csl = ref != null ? ref.get() : null; if (csl != null) { return csl; } @@ -118,8 +117,7 @@ public class ColorStateList implements Parcelable { */ public static ColorStateList createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException { - - AttributeSet attrs = Xml.asAttributeSet(parser); + final AttributeSet attrs = Xml.asAttributeSet(parser); int type; while ((type=parser.next()) != XmlPullParser.START_TAG @@ -133,22 +131,22 @@ public class ColorStateList implements Parcelable { return createFromXmlInner(r, parser, attrs); } - /* Create from inside an XML document. Called on a parser positioned at - * a tag in an XML document, tries to create a ColorStateList from that tag. - * Returns null if the tag is not a valid ColorStateList. + /** + * Create from inside an XML document. Called on a parser positioned at a + * tag in an XML document, tries to create a ColorStateList from that tag. + * + * @throws XmlPullParserException if the current tag is not <selector> + * @return A color state list for the current tag. */ private static ColorStateList createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { - - ColorStateList colorStateList; - + final ColorStateList colorStateList; final String name = parser.getName(); - if (name.equals("selector")) { colorStateList = new ColorStateList(); } else { throw new XmlPullParserException( - parser.getPositionDescription() + ": invalid drawable tag " + name); + parser.getPositionDescription() + ": invalid drawable tag " + name); } colorStateList.inflate(r, parser, attrs); @@ -161,9 +159,8 @@ public class ColorStateList implements Parcelable { * (0-255). */ public ColorStateList withAlpha(int alpha) { - int[] colors = new int[mColors.length]; - - int len = colors.length; + final int[] colors = new int[mColors.length]; + final int len = colors.length; for (int i = 0; i < len; i++) { colors[i] = (mColors[i] & 0xFFFFFF) | (alpha << 24); } @@ -176,7 +173,6 @@ public class ColorStateList implements Parcelable { */ private void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) throws XmlPullParserException, IOException { - int type; final int innerDepth = parser.getDepth()+1; @@ -259,10 +255,25 @@ public class ColorStateList implements Parcelable { System.arraycopy(stateSpecList, 0, mStateSpecs, 0, listSize); } + /** + * Indicates whether this color state list contains more than one state spec + * and will change color based on state. + * + * @return True if this color state list changes color based on state, false + * otherwise. + * @see #getColorForState(int[], int) + */ public boolean isStateful() { return mStateSpecs.length > 1; } + /** + * Indicates whether this color state list is opaque, which means that every + * color returned from {@link #getColorForState(int[], int)} has an alpha + * value of 255. + * + * @return True if this color state list is opaque. + */ public boolean isOpaque() { final int n = mColors.length; for (int i = 0; i < n; i++) { diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index 499de17..ed3f9aa 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -21,6 +21,7 @@ import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; +import android.annotation.Nullable; import android.content.pm.ActivityInfo; import android.graphics.Movie; import android.graphics.drawable.Drawable; @@ -30,11 +31,11 @@ import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Trace; +import android.util.ArrayMap; 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; @@ -103,10 +104,10 @@ public class Resources { // 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 ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache = + new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); + private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache = + new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>(); private final LongSparseArray<WeakReference<ColorStateList>> mColorStateListCache = new LongSparseArray<WeakReference<ColorStateList>>(); @@ -701,12 +702,17 @@ public class Resources { * Context.obtainStyledAttributes} with * an array containing the resource ID of interest to create the TypedArray.</p> * + * <p class="note"><strong>Note:</strong> To obtain a themed drawable, use + * {@link android.content.Context#getDrawable(int) Context.getDrawable(int)} + * or {@link #getDrawable(int, Theme)} passing the desired theme.</p> + * * @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. + * @see #getDrawable(int, Theme) */ public Drawable getDrawable(int id) throws NotFoundException { return getDrawable(id, null); @@ -714,17 +720,19 @@ public class Resources { /** * Return a drawable object associated with a particular resource ID and - * styled for the specified theme. + * styled for the specified theme. Various types of objects will be + * returned depending on the underlying resource -- for example, a solid + * color, PNG image, scalable image, etc. * * @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. + * @param theme The theme used to style the drawable attributes, may be {@code null}. * @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 { + public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -754,6 +762,11 @@ public class Resources { * image, scalable image, etc. The Drawable API hides these implementation * details. * + * <p class="note"><strong>Note:</strong> To obtain a themed drawable, use + * {@link android.content.Context#getDrawable(int) Context.getDrawable(int)} + * or {@link #getDrawableForDensity(int, int, Theme)} passing the desired + * theme.</p> + * * @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. @@ -777,12 +790,12 @@ public class Resources { * 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. + * @param theme The theme used to style the drawable attributes, may be {@code null}. * @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) { + public Drawable getDrawableForDensity(int id, int density, @Nullable Theme theme) { TypedValue value; synchronized (mAccessLock) { value = mTmpValue; @@ -1260,18 +1273,17 @@ public class Resources { * any of the style's attributes are already defined in the theme, the * current values in the theme will be overwritten. * - * @param resid The resource ID of a style resource from which to + * @param resId The resource ID of a style resource from which to * obtain attribute values. * @param force If true, values in the style resource will always be * used in the theme; otherwise, they will only be used * if not already defined in the theme. */ - public void applyStyle(int resid, boolean force) { - AssetManager.applyThemeStyle(mTheme, resid, force); + 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; + mThemeResId = resId; + mKey += Integer.toHexString(resId) + (force ? "! " : " "); } /** @@ -1287,6 +1299,7 @@ public class Resources { AssetManager.copyTheme(mTheme, other.mTheme); mThemeResId = other.mThemeResId; + mKey = other.mKey; } /** @@ -1475,21 +1488,12 @@ public class Resources { * 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) { + public TypedArray resolveAttributes(int[] values, int[] attrs) { final int len = attrs.length; if (values != null && len != values.length) { throw new IllegalArgumentException( @@ -1497,8 +1501,7 @@ public class Resources { } final TypedArray array = TypedArray.obtain(Resources.this, len); - AssetManager.resolveAttrs(mTheme, defStyleAttr, defStyleRes, - values, attrs, array.mData, array.mIndices); + AssetManager.resolveAttrs(mTheme, 0, 0, values, attrs, array.mData, array.mIndices); array.mTheme = this; array.mXml = null; @@ -1586,6 +1589,9 @@ public class Resources { /** Resource identifier for the theme. */ private int mThemeResId = 0; + /** Unique key for the series of styles applied to this theme. */ + private String mKey = ""; + // Needed by layoutlib. /*package*/ long getNativeTheme() { return mTheme; @@ -1594,6 +1600,10 @@ public class Resources { /*package*/ int getAppliedStyleResId() { return mThemeResId; } + + /*package*/ String getKey() { + return mKey; + } } /** @@ -1749,7 +1759,8 @@ public class Resources { } private void clearDrawableCachesLocked( - ThemedCaches<ConstantState> caches, int configChanges) { + ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches, + int configChanges) { final int N = caches.size(); for (int i = 0; i < N; i++) { clearDrawableCacheLocked(caches.valueAt(i), configChanges); @@ -1772,7 +1783,7 @@ public class Resources { configChanges, cs.getChangingConfigurations())) { if (DEBUG_CONFIG) { Log.d(TAG, "FLUSHING #0x" - + Long.toHexString(mDrawableCache.keyAt(i)) + + Long.toHexString(cache.keyAt(i)) + " / " + cs + " with changes: 0x" + Integer.toHexString(cs.getChangingConfigurations())); } @@ -2214,7 +2225,7 @@ public class Resources { } final boolean isColorDrawable; - final ThemedCaches<ConstantState> caches; + final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches; final long key; if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT && value.type <= TypedValue.TYPE_LAST_COLOR_INT) { @@ -2267,7 +2278,8 @@ public class Resources { } private void cacheDrawable(TypedValue value, Theme theme, boolean isColorDrawable, - ThemedCaches<ConstantState> caches, long key, Drawable dr) { + ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches, + long key, Drawable dr) { final ConstantState cs = dr.getConstantState(); if (cs == null) { return; @@ -2296,8 +2308,12 @@ public class Resources { } } else { synchronized (mAccessLock) { - final LongSparseArray<WeakReference<ConstantState>> themedCache; - themedCache = caches.getOrCreate(theme == null ? 0 : theme.mThemeResId); + final String themeKey = theme == null ? "" : theme.mKey; + LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey); + if (themedCache == null) { + themedCache = new LongSparseArray<WeakReference<ConstantState>>(1); + caches.put(themeKey, themedCache); + } themedCache.put(key, new WeakReference<ConstantState>(cs)); } } @@ -2336,12 +2352,12 @@ public class Resources { if (file.endsWith(".xml")) { final XmlResourceParser rp = loadXmlResourceParser( file, id, value.assetCookie, "drawable"); - dr = Drawable.createFromXmlThemed(this, rp, theme); + dr = Drawable.createFromXml(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); + dr = Drawable.createFromResourceStream(this, value, is, file, null); is.close(); } } catch (Exception e) { @@ -2356,7 +2372,9 @@ public class Resources { return dr; } - private Drawable getCachedDrawable(ThemedCaches<ConstantState> caches, long key, Theme theme) { + private Drawable getCachedDrawable( + ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches, + long key, Theme theme) { synchronized (mAccessLock) { final int themeKey = theme != null ? theme.mThemeResId : 0; final LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey); @@ -2593,21 +2611,4 @@ 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 15337ce..20dcf83 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -885,13 +885,13 @@ public class TypedArray { /** * Extracts theme attributes from a typed array for later resolution using - * {@link Theme#resolveAttributes(int[], int[], int, int)}. + * {@link Theme#resolveAttributes(int[], int[])}. Removes the entries from + * the typed array so that subsequent calls to typed getters will return the + * default value without crashing. * - * @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 + * attributes, or null if there are no theme attributes in the typed + * array * @hide */ public int[] extractThemeAttrs() { @@ -901,15 +901,27 @@ public class TypedArray { int[] attrs = null; + final int[] data = mData; 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; + final int index = i * AssetManager.STYLE_NUM_ENTRIES; + if (data[index + AssetManager.STYLE_TYPE] != TypedValue.TYPE_ATTRIBUTE) { + continue; + } + + // Null the entry so that we can safely call getZzz(). + data[index + AssetManager.STYLE_TYPE] = TypedValue.TYPE_NULL; + + final int attr = data[index + AssetManager.STYLE_DATA]; + if (attr == 0) { + // This attribute is useless! + continue; + } + + if (attrs == null) { + attrs = new int[N]; } + attrs[i] = attr; } return attrs; diff --git a/core/java/android/database/CursorWindow.java b/core/java/android/database/CursorWindow.java index 197e3ff..a75372f 100644 --- a/core/java/android/database/CursorWindow.java +++ b/core/java/android/database/CursorWindow.java @@ -42,12 +42,8 @@ import android.util.LongSparseArray; public class CursorWindow extends SQLiteClosable implements Parcelable { private static final String STATS_TAG = "CursorWindowStats"; - /** The cursor window size. resource xml file specifies the value in kB. - * convert it to bytes here by multiplying with 1024. - */ - private static final int sCursorWindowSize = - Resources.getSystem().getInteger( - com.android.internal.R.integer.config_cursorWindowSize) * 1024; + // This static member will be evaluated when first used. + private static int sCursorWindowSize = -1; /** * The native CursorWindow object pointer. (FOR INTERNAL USE ONLY) @@ -100,6 +96,13 @@ public class CursorWindow extends SQLiteClosable implements Parcelable { public CursorWindow(String name) { mStartPos = 0; mName = name != null && name.length() != 0 ? name : "<unnamed>"; + if (sCursorWindowSize < 0) { + /** The cursor window size. resource xml file specifies the value in kB. + * convert it to bytes here by multiplying with 1024. + */ + sCursorWindowSize = Resources.getSystem().getInteger( + com.android.internal.R.integer.config_cursorWindowSize) * 1024; + } mWindowPtr = nativeCreate(mName, sCursorWindowSize); if (mWindowPtr == 0) { throw new CursorWindowAllocationException("Cursor window allocation of " + diff --git a/core/java/android/ddm/DdmHandleHello.java b/core/java/android/ddm/DdmHandleHello.java index 220b40d..2dce425 100644 --- a/core/java/android/ddm/DdmHandleHello.java +++ b/core/java/android/ddm/DdmHandleHello.java @@ -22,6 +22,7 @@ import org.apache.harmony.dalvik.ddmc.DdmServer; import android.util.Log; import android.os.Debug; import android.os.UserHandle; +import dalvik.system.VMRuntime; import java.nio.ByteBuffer; @@ -126,8 +127,21 @@ public class DdmHandleHello extends ChunkHandler { // appName = "unknown"; String appName = DdmHandleAppName.getAppName(); - ByteBuffer out = ByteBuffer.allocate(20 - + vmIdent.length()*2 + appName.length()*2); + VMRuntime vmRuntime = VMRuntime.getRuntime(); + String instructionSetDescription = + vmRuntime.is64Bit() ? "64-bit" : "32-bit"; + String vmInstructionSet = vmRuntime.vmInstructionSet(); + if (vmInstructionSet != null && vmInstructionSet.length() > 0) { + instructionSetDescription += " (" + vmInstructionSet + ")"; + } + String vmFlags = "CheckJNI=" + + (vmRuntime.isCheckJniEnabled() ? "true" : "false"); + + ByteBuffer out = ByteBuffer.allocate(28 + + vmIdent.length() * 2 + + appName.length() * 2 + + instructionSetDescription.length() * 2 + + vmFlags.length() * 2); out.order(ChunkHandler.CHUNK_ORDER); out.putInt(DdmServer.CLIENT_PROTOCOL_VERSION); out.putInt(android.os.Process.myPid()); @@ -136,6 +150,10 @@ public class DdmHandleHello extends ChunkHandler { putString(out, vmIdent); putString(out, appName); out.putInt(UserHandle.myUserId()); + out.putInt(instructionSetDescription.length()); + putString(out, instructionSetDescription); + out.putInt(vmFlags.length()); + putString(out, vmFlags); Chunk reply = new Chunk(CHUNK_HELO, out); diff --git a/core/java/android/hardware/Camera.java b/core/java/android/hardware/Camera.java index 35c86e7..0705e0c 100644 --- a/core/java/android/hardware/Camera.java +++ b/core/java/android/hardware/Camera.java @@ -169,6 +169,10 @@ public class Camera { private boolean mFaceDetectionRunning = false; private Object mAutoFocusCallbackLock = new Object(); + private static final int NO_ERROR = 0; + private static final int EACCESS = -13; + private static final int ENODEV = -19; + /** * Broadcast Action: A new picture is taken by the camera, and the entry of * the picture has been added to the media store. @@ -328,6 +332,24 @@ public class Camera { } Camera(int cameraId) { + int err = cameraInit(cameraId); + if (checkInitErrors(err)) { + switch(err) { + case EACCESS: + throw new RuntimeException("Fail to connect to camera service"); + case ENODEV: + throw new RuntimeException("Camera initialization failed"); + default: + // Should never hit this. + throw new RuntimeException("Unknown camera error"); + } + } + } + + /** + * @hide + */ + public int cameraInit(int cameraId) { mShutterCallback = null; mRawImageCallback = null; mJpegCallback = null; @@ -347,7 +369,21 @@ public class Camera { String packageName = ActivityThread.currentPackageName(); - native_setup(new WeakReference<Camera>(this), cameraId, packageName); + return native_setup(new WeakReference<Camera>(this), cameraId, packageName); + } + + /** + * @hide + */ + public static boolean checkInitErrors(int err) { + return err != NO_ERROR; + } + + /** + * @hide + */ + public static Camera openUninitialized() { + return new Camera(); } /** @@ -360,7 +396,7 @@ public class Camera { release(); } - private native final void native_setup(Object camera_this, int cameraId, + private native final int native_setup(Object camera_this, int cameraId, String packageName); private native final void native_release(); @@ -458,13 +494,16 @@ public class Camera { */ public final void setPreviewDisplay(SurfaceHolder holder) throws IOException { if (holder != null) { - setPreviewDisplay(holder.getSurface()); + setPreviewSurface(holder.getSurface()); } else { - setPreviewDisplay((Surface)null); + setPreviewSurface((Surface)null); } } - private native final void setPreviewDisplay(Surface surface) throws IOException; + /** + * @hide + */ + public native final void setPreviewSurface(Surface surface) throws IOException; /** * Sets the {@link SurfaceTexture} to be used for live preview. diff --git a/core/java/android/hardware/Sensor.java b/core/java/android/hardware/Sensor.java index 4bea9ee..c8de2f1 100644 --- a/core/java/android/hardware/Sensor.java +++ b/core/java/android/hardware/Sensor.java @@ -142,9 +142,10 @@ public final class Sensor { public static final String STRING_TYPE_TEMPERATURE = "android.sensor.temperature"; /** - * A constant describing a proximity sensor type. + * A constant describing a proximity sensor type. This is a wake up sensor. * <p>See {@link android.hardware.SensorEvent#values SensorEvent.values} * for more details. + * @see #isWakeUpSensor() */ public static final int TYPE_PROXIMITY = 8; @@ -307,8 +308,10 @@ public final class Sensor { * itself. The sensor continues to operate while the device is asleep * and will automatically wake the device to notify when significant * motion is detected. The application does not need to hold any wake - * locks for this sensor to trigger. + * locks for this sensor to trigger. This is a wake up sensor. * <p>See {@link TriggerEvent} for more details. + * + * @see #isWakeUpSensor() */ public static final int TYPE_SIGNIFICANT_MOTION = 17; @@ -381,11 +384,17 @@ public final class Sensor { /** * A constant describing a heart rate monitor. * <p> - * A sensor that measures the heart rate in beats per minute. + * The reported value is the heart rate in beats per minute. + * <p> + * The reported accuracy represents the status of the monitor during the reading. See the + * {@code SENSOR_STATUS_*} constants in {@link android.hardware.SensorManager SensorManager} + * for more details on accuracy/status values. In particular, when the accuracy is + * {@code SENSOR_STATUS_UNRELIABLE} or {@code SENSOR_STATUS_NO_CONTACT}, the heart rate + * value should be discarded. * <p> - * value[0] represents the beats per minute when the measurement was taken. - * value[0] is 0 if the heart rate monitor could not measure the rate or the - * rate is 0 beat per minute. + * This sensor requires permission {@code android.permission.BODY_SENSORS}. + * It will not be returned by {@code SensorManager.getSensorsList} nor + * {@code SensorManager.getDefaultSensor} if the application doesn't have this permission. */ public static final int TYPE_HEART_RATE = 21; @@ -397,6 +406,348 @@ public final class Sensor { public static final String STRING_TYPE_HEART_RATE = "android.sensor.heart_rate"; /** + * A non-wake up variant of proximity sensor. + * + * @see #TYPE_PROXIMITY + */ + public static final int TYPE_NON_WAKE_UP_PROXIMITY_SENSOR = 22; + + /** + * A constant string describing a non-wake up proximity sensor type. + * + * @see #TYPE_NON_WAKE_UP_PROXIMITY_SENSOR + */ + public static final String SENSOR_STRING_TYPE_NON_WAKE_UP_PROXIMITY_SENSOR = + "android.sensor.non_wake_up_proximity_sensor"; + + /** + * A constant describing a wake up variant of accelerometer sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_ACCELEROMETER + */ + public static final int TYPE_WAKE_UP_ACCELEROMETER = 23; + + /** + * A constant string describing a wake up accelerometer. + * + * @see #TYPE_WAKE_UP_ACCELEROMETER + */ + public static final String STRING_TYPE_WAKE_UP_ACCELEROMETER = + "android.sensor.wake_up_accelerometer"; + + /** + * A constant describing a wake up variant of a magnetic field sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_MAGNETIC_FIELD + */ + public static final int TYPE_WAKE_UP_MAGNETIC_FIELD = 24; + + /** + * A constant string describing a wake up magnetic field sensor. + * + * @see #TYPE_WAKE_UP_MAGNETIC_FIELD + */ + public static final String STRING_TYPE_WAKE_UP_MAGNETIC_FIELD = + "android.sensor.wake_up_magnetic_field"; + + /** + * A constant describing a wake up variant of an orientation sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_ORIENTATION + */ + public static final int TYPE_WAKE_UP_ORIENTATION = 25; + + /** + * A constant string describing a wake up orientation sensor. + * + * @see #TYPE_WAKE_UP_ORIENTATION + */ + public static final String STRING_TYPE_WAKE_UP_ORIENTATION = + "android.sensor.wake_up_orientation"; + + /** + * A constant describing a wake up variant of a gyroscope sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_GYROSCOPE + */ + public static final int TYPE_WAKE_UP_GYROSCOPE = 26; + + /** + * A constant string describing a wake up gyroscope sensor type. + * + * @see #TYPE_WAKE_UP_GYROSCOPE + */ + public static final String STRING_TYPE_WAKE_UP_GYROSCOPE = "android.sensor.wake_up_gyroscope"; + + /** + * A constant describing a wake up variant of a light sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_LIGHT + */ + public static final int TYPE_WAKE_UP_LIGHT = 27; + + /** + * A constant string describing a wake up light sensor type. + * + * @see #TYPE_WAKE_UP_LIGHT + */ + public static final String STRING_TYPE_WAKE_UP_LIGHT = "android.sensor.wake_up_light"; + + /** + * A constant describing a wake up variant of a pressure sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_PRESSURE + */ + public static final int TYPE_WAKE_UP_PRESSURE = 28; + + /** + * A constant string describing a wake up pressure sensor type. + * + * @see #TYPE_WAKE_UP_PRESSURE + */ + public static final String STRING_TYPE_WAKE_UP_PRESSURE = "android.sensor.wake_up_pressure"; + + /** + * A constant describing a wake up variant of a gravity sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_GRAVITY + */ + public static final int TYPE_WAKE_UP_GRAVITY = 29; + + /** + * A constant string describing a wake up gravity sensor type. + * + * @see #TYPE_WAKE_UP_GRAVITY + */ + public static final String STRING_TYPE_WAKE_UP_GRAVITY = "android.sensor.wake_up_gravity"; + + /** + * A constant describing a wake up variant of a linear acceleration sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_LINEAR_ACCELERATION + */ + public static final int TYPE_WAKE_UP_LINEAR_ACCELERATION = 30; + + /** + * A constant string describing a wake up linear acceleration sensor type. + * + * @see #TYPE_WAKE_UP_LINEAR_ACCELERATION + */ + public static final String STRING_TYPE_WAKE_UP_LINEAR_ACCELERATION = + "android.sensor.wake_up_linear_acceleration"; + + /** + * A constant describing a wake up variant of a rotation vector sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_ROTATION_VECTOR + */ + public static final int TYPE_WAKE_UP_ROTATION_VECTOR = 31; + + /** + * A constant string describing a wake up rotation vector sensor type. + * + * @see #TYPE_WAKE_UP_ROTATION_VECTOR + */ + public static final String STRING_TYPE_WAKE_UP_ROTATION_VECTOR = + "android.sensor.wake_up_rotation_vector"; + + /** + * A constant describing a wake up variant of a relative humidity sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_RELATIVE_HUMIDITY + */ + public static final int TYPE_WAKE_UP_RELATIVE_HUMIDITY = 32; + + /** + * A constant string describing a wake up relative humidity sensor type. + * + * @see #TYPE_WAKE_UP_RELATIVE_HUMIDITY + */ + public static final String STRING_TYPE_WAKE_UP_RELATIVE_HUMIDITY = + "android.sensor.wake_up_relative_humidity"; + + /** + * A constant describing a wake up variant of an ambient temperature sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_AMBIENT_TEMPERATURE + */ + public static final int TYPE_WAKE_UP_AMBIENT_TEMPERATURE = 33; + + /** + * A constant string describing a wake up ambient temperature sensor type. + * + * @see #TYPE_WAKE_UP_AMBIENT_TEMPERATURE + */ + public static final String STRING_TYPE_WAKE_UP_AMBIENT_TEMPERATURE = + "android.sensor.wake_up_ambient_temperature"; + + /** + * A constant describing a wake up variant of an uncalibrated magnetic field sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_MAGNETIC_FIELD_UNCALIBRATED + */ + public static final int TYPE_WAKE_UP_MAGNETIC_FIELD_UNCALIBRATED = 34; + + /** + * A constant string describing a wake up uncalibrated magnetic field sensor type. + * + * @see #TYPE_WAKE_UP_MAGNETIC_FIELD_UNCALIBRATED + */ + public static final String STRING_TYPE_WAKE_UP_MAGNETIC_FIELD_UNCALIBRATED = + "android.sensor.wake_up_magnetic_field_uncalibrated"; + + /** + * A constant describing a wake up variant of a game rotation vector sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_GAME_ROTATION_VECTOR + */ + public static final int TYPE_WAKE_UP_GAME_ROTATION_VECTOR = 35; + + /** + * A constant string describing a wake up game rotation vector sensor type. + * + * @see #TYPE_WAKE_UP_GAME_ROTATION_VECTOR + */ + public static final String STRING_TYPE_WAKE_UP_GAME_ROTATION_VECTOR = + "android.sensor.wake_up_game_rotation_vector"; + + /** + * A constant describing a wake up variant of an uncalibrated gyroscope sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_GYROSCOPE_UNCALIBRATED + */ + public static final int TYPE_WAKE_UP_GYROSCOPE_UNCALIBRATED = 36; + + /** + * A constant string describing a wake up uncalibrated gyroscope sensor type. + * + * @see #TYPE_WAKE_UP_GYROSCOPE_UNCALIBRATED + */ + public static final String STRING_TYPE_WAKE_UP_GYROSCOPE_UNCALIBRATED = + "android.sensor.wake_up_gyroscope_uncalibrated"; + + /** + * A constant describing a wake up variant of a step detector sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_STEP_DETECTOR + */ + public static final int TYPE_WAKE_UP_STEP_DETECTOR = 37; + + /** + * A constant string describing a wake up step detector sensor type. + * + * @see #TYPE_WAKE_UP_STEP_DETECTOR + */ + public static final String STRING_TYPE_WAKE_UP_STEP_DETECTOR = + "android.sensor.wake_up_step_detector"; + + /** + * A constant describing a wake up variant of a step counter sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_STEP_COUNTER + */ + public static final int TYPE_WAKE_UP_STEP_COUNTER = 38; + + /** + * A constant string describing a wake up step counter sensor type. + * + * @see #TYPE_WAKE_UP_STEP_COUNTER + */ + public static final String STRING_TYPE_WAKE_UP_STEP_COUNTER = + "android.sensor.wake_up_step_counter"; + + /** + * A constant describing a wake up variant of a geomagnetic rotation vector sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_GEOMAGNETIC_ROTATION_VECTOR + */ + public static final int TYPE_WAKE_UP_GEOMAGNETIC_ROTATION_VECTOR = 39; + + /** + * A constant string describing a wake up geomagnetic rotation vector sensor type. + * + * @see #TYPE_WAKE_UP_GEOMAGNETIC_ROTATION_VECTOR + */ + public static final String STRING_TYPE_WAKE_UP_GEOMAGNETIC_ROTATION_VECTOR = + "android.sensor.wake_up_geomagnetic_rotation_vector"; + + /** + * A constant describing a wake up variant of a heart rate sensor type. + * + * @see #isWakeUpSensor() + * @see #TYPE_HEART_RATE + */ + public static final int TYPE_WAKE_UP_HEART_RATE = 40; + + /** + * A constant string describing a wake up heart rate sensor type. + * + * @see #TYPE_WAKE_UP_HEART_RATE + */ + public static final String STRING_TYPE_WAKE_UP_HEART_RATE = "android.sensor.wake_up_heart_rate"; + + /** + * A sensor of this type generates an event each time a tilt event is detected. A tilt event + * is generated if the direction of the 2-seconds window average gravity changed by at + * least 35 degrees since the activation of the sensor. It is a wake up sensor. + * + * @see #isWakeUpSensor() + */ + public static final int TYPE_WAKE_UP_TILT_DETECTOR = 41; + + /** + * A constant string describing a wake up tilt detector sensor type. + * + * @see #TYPE_WAKE_UP_TILT_DETECTOR + */ + public static final String SENSOR_STRING_TYPE_WAKE_UP_TILT_DETECTOR = + "android.sensor.wake_up_tilt_detector"; + + /** + * A constant describing a wake gesture sensor. + * <p> + * Wake gesture sensors enable waking up the device based on a device specific motion. + * <p> + * When this sensor triggers, the device behaves as if the power button was pressed, turning the + * screen on. This behavior (turning on the screen when this sensor triggers) might be + * deactivated by the user in the device settings. Changes in settings do not impact the + * behavior of the sensor: only whether the framework turns the screen on when it triggers. + * <p> + * The actual gesture to be detected is not specified, and can be chosen by the manufacturer of + * the device. This sensor must be low power, as it is likely to be activated 24/7. + * Values of events created by this sensors should not be used. + * + * @see #isWakeUpSensor() + * @hide This sensor is expected to only be used by the power manager + */ + public static final int TYPE_WAKE_GESTURE = 42; + + /** + * A constant string describing a wake gesture sensor. + * + * @hide This sensor is expected to only be used by the power manager + * @see #TYPE_WAKE_GESTURE + */ + public static final String STRING_TYPE_WAKE_GESTURE = "android.sensor.wake_gesture"; + + /** * A constant describing all sensor types. */ public static final int TYPE_ALL = -1; @@ -441,7 +792,29 @@ public final class Sensor { REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_STEP_DETECTOR REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_STEP_COUNTER REPORTING_MODE_CONTINUOUS, 5, // SENSOR_TYPE_GEOMAGNETIC_ROTATION_VECTOR - REPORTING_MODE_ON_CHANGE, 1 // SENSOR_TYPE_HEART_RATE_MONITOR + REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_HEART_RATE_MONITOR + REPORTING_MODE_ON_CHANGE, 3, // SENSOR_TYPE_NON_WAKE_UP_PROXIMITY_SENSOR + // wake up variants of base sensors + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_ACCELEROMETER + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_MAGNETIC_FIELD + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_ORIENTATION + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_GYROSCOPE + REPORTING_MODE_ON_CHANGE, 3, // SENSOR_TYPE_WAKE_UP_LIGHT + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_PRESSURE + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_GRAVITY + REPORTING_MODE_CONTINUOUS, 3, // SENSOR_TYPE_WAKE_UP_LINEAR_ACCELERATION + REPORTING_MODE_CONTINUOUS, 5, // SENSOR_TYPE_WAKE_UP_ROTATION_VECTOR + REPORTING_MODE_ON_CHANGE, 3, // SENSOR_TYPE_WAKE_UP_RELATIVE_HUMIDITY + REPORTING_MODE_ON_CHANGE, 3, // SENSOR_TYPE_WAKE_UP_AMBIENT_TEMPERATURE + REPORTING_MODE_CONTINUOUS, 6, // SENSOR_TYPE_WAKE_UP_MAGNETIC_FIELD_UNCALIBRATED + REPORTING_MODE_CONTINUOUS, 4, // SENSOR_TYPE_WAKE_UP_GAME_ROTATION_VECTOR + REPORTING_MODE_CONTINUOUS, 6, // SENSOR_TYPE_WAKE_UP_GYROSCOPE_UNCALIBRATED + REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_WAKE_UP_STEP_DETECTOR + REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_WAKE_UP_STEP_COUNTER + REPORTING_MODE_CONTINUOUS, 5, // SENSOR_TYPE_WAKE_UP_GEOMAGNETIC_ROTATION_VECTOR + REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_WAKE_UP_HEART_RATE_MONITOR + REPORTING_MODE_ON_CHANGE, 1, // SENSOR_TYPE_WAKE_UP_TILT_DETECTOR + REPORTING_MODE_ONE_SHOT, 1, // SENSOR_TYPE_WAKE_GESTURE }; static int getReportingMode(Sensor sensor) { @@ -499,6 +872,8 @@ public final class Sensor { private int mFifoMaxEventCount; private String mStringType; private String mRequiredPermission; + private int mMaxDelay; + private boolean mWakeUpSensor; Sensor() { } @@ -587,6 +962,7 @@ public final class Sensor { } /** + * @hide * @return The permission required to access this sensor. If empty, no permission is required. */ public String getRequiredPermission() { @@ -598,6 +974,51 @@ public final class Sensor { return mHandle; } + /** + * This value is defined only for continuous mode sensors. It is the delay between two + * sensor events corresponding to the lowest frequency that this sensor supports. When + * lower frequencies are requested through registerListener() the events will be generated + * at this frequency instead. It can be used to estimate when the batch FIFO may be full. + * Older devices may set this value to zero. Ignore this value in case it is negative + * or zero. + * + * @return The max delay for this sensor in microseconds. + */ + public int getMaxDelay() { + return mMaxDelay; + } + + /** + * Returns whether this sensor is a wake-up sensor. + * <p> + * Wake up sensors wake the application processor up when they have events to deliver. When a + * wake up sensor is registered to without batching enabled, each event will wake the + * application processor up. + * <p> + * When a wake up sensor is registered to with batching enabled, it + * wakes the application processor up when maxReportingLatency has elapsed or when the hardware + * FIFO storing the events from wake up sensors is getting full. + * <p> + * Non-wake up sensors never wake the application processor up. Their events are only reported + * when the application processor is awake, for example because the application holds a wake + * lock, or another source woke the application processor up. + * <p> + * When a non-wake up sensor is registered to without batching enabled, the measurements made + * while the application processor is asleep might be lost and never returned. + * <p> + * When a non-wake up sensor is registered to with batching enabled, the measurements made while + * the application processor is asleep are stored in the hardware FIFO for non-wake up sensors. + * When this FIFO gets full, new events start overwriting older events. When the application + * then wakes up, the latest events are returned, and some old events might be lost. The number + * of events actually returned depends on the hardware FIFO size, as well as on what other + * sensors are activated. If losing sensor events is not acceptable during batching, you must + * use the wake-up version of the sensor. + * @return true if this is a wake up sensor, false otherwise. + */ + public boolean isWakeUpSensor() { + return mWakeUpSensor; + } + void setRange(float max, float res) { mMaxRange = max; mResolution = res; diff --git a/core/java/android/hardware/SensorEventListener.java b/core/java/android/hardware/SensorEventListener.java index 677d244..0d859fb 100644 --- a/core/java/android/hardware/SensorEventListener.java +++ b/core/java/android/hardware/SensorEventListener.java @@ -39,11 +39,13 @@ public interface SensorEventListener { public void onSensorChanged(SensorEvent event); /** - * Called when the accuracy of a sensor has changed. - * <p>See {@link android.hardware.SensorManager SensorManager} - * for details. + * Called when the accuracy of the registered sensor has changed. + * + * <p>See the SENSOR_STATUS_* constants in + * {@link android.hardware.SensorManager SensorManager} for details. * - * @param accuracy The new accuracy of this sensor + * @param accuracy The new accuracy of this sensor, one of + * {@code SensorManager.SENSOR_STATUS_*} */ - public void onAccuracyChanged(Sensor sensor, int accuracy); + public void onAccuracyChanged(Sensor sensor, int accuracy); } diff --git a/core/java/android/hardware/SensorManager.java b/core/java/android/hardware/SensorManager.java index 5f2b5f0..25c7630 100644 --- a/core/java/android/hardware/SensorManager.java +++ b/core/java/android/hardware/SensorManager.java @@ -321,6 +321,13 @@ public abstract class SensorManager { /** + * The values returned by this sensor cannot be trusted because the sensor + * had no contact with what it was measuring (for example, the heart rate + * monitor is not in contact with the user). + */ + public static final int SENSOR_STATUS_NO_CONTACT = -1; + + /** * The values returned by this sensor cannot be trusted, calibration is * needed or the environment doesn't allow readings */ @@ -421,9 +428,10 @@ public abstract class SensorManager { * {@link SensorManager#getSensorList(int) getSensorList}. * * @param type - * of sensors requested + * of sensors requested * - * @return the default sensors matching the asked type. + * @return the default sensor matching the requested type if one exists and the application + * has the necessary permissions, or null otherwise. * * @see #getSensorList(int) * @see Sensor diff --git a/core/java/android/hardware/SystemSensorManager.java b/core/java/android/hardware/SystemSensorManager.java index 8684a04..b66ec86 100644 --- a/core/java/android/hardware/SystemSensorManager.java +++ b/core/java/android/hardware/SystemSensorManager.java @@ -395,25 +395,12 @@ public class SystemSensorManager extends SensorManager { t.timestamp = timestamp; t.accuracy = inAccuracy; t.sensor = sensor; - switch (t.sensor.getType()) { - // Only report accuracy for sensors that support it. - case Sensor.TYPE_MAGNETIC_FIELD: - case Sensor.TYPE_ORIENTATION: - // call onAccuracyChanged() only if the value changes - final int accuracy = mSensorAccuracies.get(handle); - if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { - mSensorAccuracies.put(handle, t.accuracy); - mListener.onAccuracyChanged(t.sensor, t.accuracy); - } - break; - default: - // For other sensors, just report the accuracy once - if (mFirstEvent.get(handle) == false) { - mFirstEvent.put(handle, true); - mListener.onAccuracyChanged( - t.sensor, SENSOR_STATUS_ACCURACY_HIGH); - } - break; + + // call onAccuracyChanged() only if the value changes + final int accuracy = mSensorAccuracies.get(handle); + if ((t.accuracy >= 0) && (accuracy != t.accuracy)) { + mSensorAccuracies.put(handle, t.accuracy); + mListener.onAccuracyChanged(t.sensor, t.accuracy); } mListener.onSensorChanged(t); } diff --git a/core/java/android/hardware/camera2/CameraAccessException.java b/core/java/android/hardware/camera2/CameraAccessException.java index 1af575f..ca71e81 100644 --- a/core/java/android/hardware/camera2/CameraAccessException.java +++ b/core/java/android/hardware/camera2/CameraAccessException.java @@ -114,7 +114,10 @@ public class CameraAccessException extends AndroidException { mReason = problem; } - private static String getDefaultMessage(int problem) { + /** + * @hide + */ + public static String getDefaultMessage(int problem) { switch (problem) { case CAMERA_IN_USE: return "The camera device is in use already"; diff --git a/core/java/android/hardware/camera2/CameraCaptureSession.java b/core/java/android/hardware/camera2/CameraCaptureSession.java new file mode 100644 index 0000000..7738d2d --- /dev/null +++ b/core/java/android/hardware/camera2/CameraCaptureSession.java @@ -0,0 +1,669 @@ +/* + * 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.Handler; +import java.util.List; + +/** + * A configured capture session for a {@link CameraDevice}, used for capturing + * images from the camera. + * + * <p>A CameraCaptureSession is created by providing a set of target output surfaces to + * {@link CameraDevice#createCaptureSession createCaptureSession}. Once created, the session is + * active until a new session is created by the camera device, or the camera device is closed.</p> + * + * <p>Creating a session is an expensive operation and can take several hundred milliseconds, since + * it requires configuring the camera device's internal pipelines and allocating memory buffers for + * sending images to the desired targets. While + * {@link CameraDevice#createCaptureSession createCaptureSession} will provide a + * CameraCaptureSession object immediately, configuration won't be complete until the + * {@link CameraCaptureSession.StateListener#onConfigured onConfigured} callback is called for the + * first time. If configuration cannot be completed, then the + * {@link CameraCaptureSession.StateListener#onConfigureFailed onConfigureFailed} is called, and the + * session will not become active.</p> + * + * <p>Any capture requests (repeating or non-repeating) submitted before the session is ready will + * be queued up and will begin capture once the session becomes ready. In case the session cannot be + * configured and {@link StateListener#onConfigureFailed onConfigureFailed} is called, all queued + * capture requests are discarded.</p> + * + * <p>If a new session is created by the camera device, then the previous session is closed, and its + * associated {@link StateListener#onClosed onClosed} callback will be invoked. All + * of the session methods will throw an IllegalStateException if called once the session is + * closed.</p> + * + * <p>A closed session clears any repeating requests (as if {@link #stopRepeating} had been called), + * but will still complete all of its in-progress capture requests as normal, before a newly + * created session takes over and reconfigures the camera device.</p> + */ +public abstract class CameraCaptureSession implements AutoCloseable { + + /** + * Get the camera device that this session is created for + */ + public abstract CameraDevice getDevice(); + + /** + * <p>Submit a request for an image to be captured by the camera device.</p> + * + * <p>The request defines all the parameters for capturing the single image, + * including sensor, lens, flash, and post-processing settings.</p> + * + * <p>Each request will produce one {@link CaptureResult} and produce new frames for one or more + * target Surfaces, set with the CaptureRequest builder's + * {@link CaptureRequest.Builder#addTarget} method. The target surfaces (set with + * {@link CaptureRequest.Builder#addTarget}) must be a subset of the surfaces provided when this + * capture session was created.</p> + * + * <p>Multiple requests can be in progress at once. They are processed in + * first-in, first-out order, with minimal delays between each + * capture. Requests submitted through this method have higher priority than + * those submitted through {@link #setRepeatingRequest} or + * {@link #setRepeatingBurst}, and will be processed as soon as the current + * repeat/repeatBurst processing completes.</p> + * + * @param request the settings for this capture + * @param listener The callback object to notify once this request has been + * processed. If null, no metadata will be produced for this capture, + * although image data will still be produced. + * @param handler the handler on which the listener should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper + * looper}. + * + * @return int A unique capture sequence ID used by + * {@link CaptureListener#onCaptureSequenceCompleted}. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * @throws IllegalArgumentException if the request targets Surfaces that are not configured as + * outputs for this session. Or if the handler is null, the + * listener is not null, and the calling thread has no looper. + * + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public abstract int capture(CaptureRequest request, CaptureListener listener, Handler handler) + throws CameraAccessException; + + /** + * Submit a list of requests to be captured in sequence as a burst. The + * burst will be captured in the minimum amount of time possible, and will + * not be interleaved with requests submitted by other capture or repeat + * calls. + * + * <p>The requests will be captured in order, each capture producing one {@link CaptureResult} + * and image buffers for one or more target {@link android.view.Surface surfaces}. The target + * surfaces (set with {@link CaptureRequest.Builder#addTarget}) must be a subset of the surfaces + * provided when this capture session was created.</p> + * + * <p>The main difference between this method and simply calling + * {@link #capture} repeatedly is that this method guarantees that no + * other requests will be interspersed with the burst.</p> + * + * @param requests the list of settings for this burst capture + * @param listener The callback object to notify each time one of the + * requests in the burst has been processed. If null, no metadata will be + * produced for any requests in this burst, although image data will still + * be produced. + * @param handler the handler on which the listener should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper + * looper}. + * + * @return int A unique capture sequence ID used by + * {@link CaptureListener#onCaptureSequenceCompleted}. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * @throws IllegalArgumentException If the requests target Surfaces not currently configured as + * outputs. Or if the handler is null, the listener is not + * null, and the calling thread has no looper. + * + * @see #capture + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public abstract int captureBurst(List<CaptureRequest> requests, CaptureListener listener, + Handler handler) throws CameraAccessException; + + /** + * Request endlessly repeating capture of images by this capture session. + * + * <p>With this method, the camera device will continually capture images + * using the settings in the provided {@link CaptureRequest}, at the maximum + * rate possible.</p> + * + * <p>Repeating requests are a simple way for an application to maintain a + * preview or other continuous stream of frames, without having to + * continually submit identical requests through {@link #capture}.</p> + * + * <p>Repeat requests have lower priority than those submitted + * through {@link #capture} or {@link #captureBurst}, so if + * {@link #capture} is called when a repeating request is active, the + * capture request will be processed before any further repeating + * requests are processed.<p> + * + * <p>Repeating requests are a simple way for an application to maintain a + * preview or other continuous stream of frames, without having to submit + * requests through {@link #capture} at video rates.</p> + * + * <p>To stop the repeating capture, call {@link #stopRepeating}. Calling + * {@link #abortCaptures} will also clear the request.</p> + * + * <p>Calling this method will replace any earlier repeating request or + * burst set up by this method or {@link #setRepeatingBurst}, although any + * in-progress burst will be completed before the new repeat request will be + * used.</p> + * + * @param request the request to repeat indefinitely + * @param listener The callback object to notify every time the + * request finishes processing. If null, no metadata will be + * produced for this stream of requests, although image data will + * still be produced. + * @param handler the handler on which the listener should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper + * looper}. + * + * @return int A unique capture sequence ID used by + * {@link CaptureListener#onCaptureSequenceCompleted}. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * @throws IllegalArgumentException If the requests reference Surfaces that are not currently + * configured as outputs. Or if the handler is null, the + * listener is not null, and the calling thread has no looper. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingBurst + * @see #stopRepeating + * @see #abortCaptures + */ + public abstract int setRepeatingRequest(CaptureRequest request, CaptureListener listener, + Handler handler) throws CameraAccessException; + + /** + * <p>Request endlessly repeating capture of a sequence of images by this + * capture session.</p> + * + * <p>With this method, the camera device will continually capture images, + * cycling through the settings in the provided list of + * {@link CaptureRequest CaptureRequests}, at the maximum rate possible.</p> + * + * <p>If a request is submitted through {@link #capture} or + * {@link #captureBurst}, the current repetition of the request list will be + * completed before the higher-priority request is handled. This guarantees + * that the application always receives a complete repeat burst captured in + * minimal time, instead of bursts interleaved with higher-priority + * captures, or incomplete captures.</p> + * + * <p>Repeating burst requests are a simple way for an application to + * maintain a preview or other continuous stream of frames where each + * request is different in a predicatable way, without having to continually + * submit requests through {@link #captureBurst}.</p> + * + * <p>To stop the repeating capture, call {@link #stopRepeating}. Any + * ongoing burst will still be completed, however. Calling + * {@link #abortCaptures} will also clear the request.</p> + * + * <p>Calling this method will replace a previously-set repeating request or + * burst set up by this method or {@link #setRepeatingRequest}, although any + * in-progress burst will be completed before the new repeat burst will be + * used.</p> + * + * @param requests the list of requests to cycle through indefinitely + * @param listener The callback object to notify each time one of the + * requests in the repeating bursts has finished processing. If null, no + * metadata will be produced for this stream of requests, although image + * data will still be produced. + * @param handler the handler on which the listener should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper + * looper}. + * + * @return int A unique capture sequence ID used by + * {@link CaptureListener#onCaptureSequenceCompleted}. + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * @throws IllegalArgumentException If the requests reference Surfaces not currently configured + * as outputs. Or if the handler is null, the listener is not + * null, and the calling thread has no looper. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #stopRepeating + * @see #abortCaptures + */ + public abstract int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, + Handler handler) throws CameraAccessException; + + /** + * <p>Cancel any ongoing repeating capture set by either + * {@link #setRepeatingRequest setRepeatingRequest} or + * {@link #setRepeatingBurst}. Has no effect on requests submitted through + * {@link #capture capture} or {@link #captureBurst captureBurst}.</p> + * + * <p>Any currently in-flight captures will still complete, as will any burst that is + * mid-capture. To ensure that the device has finished processing all of its capture requests + * and is in ready state, wait for the {@link StateListener#onReady} callback after + * calling this method.</p> + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * + * @see #setRepeatingRequest + * @see #setRepeatingBurst + * @see StateListener#onIdle + */ + public abstract void stopRepeating() throws CameraAccessException; + + /** + * Discard all captures currently pending and in-progress as fast as possible. + * + * <p>The camera device will discard all of its current work as fast as possible. Some in-flight + * captures may complete successfully and call {@link CaptureListener#onCaptureCompleted}, while + * others will trigger their {@link CaptureListener#onCaptureFailed} callbacks. If a repeating + * request or a repeating burst is set, it will be cleared.</p> + * + * <p>This method is the fastest way to switch the camera device to a new session with + * {@link CameraDevice#createCaptureSession}, at the cost of discarding in-progress work. It + * must be called before the new session is created. Once all pending requests are either + * completed or thrown away, the {@link StateListener#onReady} callback will be called, + * if the session has not been closed. Otherwise, the {@link StateListener#onClosed} + * callback will be fired when a new session is created by the camera device.</p> + * + * <p>Cancelling will introduce at least a brief pause in the stream of data from the camera + * device, since once the camera device is emptied, the first new request has to make it through + * the entire camera pipeline before new output buffers are produced.</p> + * + * <p>This means that using {@code abortCaptures()} to simply remove pending requests is not + * recommended; it's best used for quickly switching output configurations, or for cancelling + * long in-progress requests (such as a multi-second capture).</p> + * + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if this session is no longer active, either because a new + * session has been created or the camera device has been closed. + * + * @see #setRepeatingRequest + * @see #setRepeatingBurst + * @see #configureOutputs + */ + public abstract void abortCaptures() throws CameraAccessException; + + /** + * Close this capture session asynchronously. + * + * <p>Closing a session frees up the target output Surfaces of the session for reuse with either a + * new session, or to other APIs that can draw to Surfaces.</p> + * + * <p>Note that creating a new capture session with {@link CameraDevice#createCaptureSession} + * will close any existing capture session automatically, and call the older session listener's + * {@link StateListener#onClosed} callback. Using {@link CameraDevice#createCaptureSession} + * directly without closing is the recommended approach for quickly switching to a new session, + * since unchanged target outputs can be reused more efficiently.</p> + * + * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and any + * repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called). + * However, any in-progress capture requests submitted to the session will be completed as + * normal; once all captures have completed and the session has been torn down, + * {@link StateListener#onClosed} will be called.</p> + */ + @Override + public abstract void close(); + + /** + * A listener for tracking the state of a camera capture session. + * + */ + public static abstract class StateListener { + + /** + * This method is called when the camera device has finished configuring itself, and the + * session can start processing capture requests. + * + * <p>If there are capture requests already queued with the session, they will start + * processing once this callback is invoked, and the session will call {@link #onActive} + * right after this callback is invoked.</p> + * + * <p>If no capture requests have been submitted, then the session will invoke + * {@link #onReady} right after this callback.</p> + * + * <p>If the camera device configuration fails, then {@link #onConfigureFailed} will + * be invoked instead of this callback.</p> + * + */ + public abstract void onConfigured(CameraCaptureSession session); + + /** + * This method is called if the session cannot be configured as requested. + * + * <p>This can happen if the set of requested outputs contains unsupported sizes, + * or too many outputs are requested at once.</p> + * + * <p>The session is considered to be closed, and all methods called on it after this + * callback is invoked will throw an IllegalStateException. Any capture requests submitted + * to the session prior to this callback will be discarded and will not produce any + * callbacks on their listeners.</p> + */ + public abstract void onConfigureFailed(CameraCaptureSession session); + + /** + * This method is called every time the session has no more capture requests to process. + * + * <p>During the creation of a new session, this callback is invoked right after + * {@link #onConfigured} if no capture requests were submitted to the session prior to it + * completing configuration.</p> + * + * <p>Otherwise, this callback will be invoked any time the session finishes processing + * all of its active capture requests, and no repeating request or burst is set up.</p> + * + */ + public void onReady(CameraCaptureSession session) { + // default empty implementation + } + + /** + * This method is called when the session starts actively processing capture requests. + * + * <p>If capture requests are submitted prior to {@link #onConfigured} being called, + * then the session will start processing those requests immediately after the callback, + * and this method will be immediately called after {@link #onConfigured}. + * + * <p>If the session runs out of capture requests to process and calls {@link #onReady}, + * then this callback will be invoked again once new requests are submitted for capture.</p> + */ + public void onActive(CameraCaptureSession session) { + // default empty implementation + } + + /** + * This method is called when the session is closed. + * + * <p>A session is closed when a new session is created by the parent camera device, + * or when the parent camera device is closed (either by the user closing the device, + * or due to a camera device disconnection or fatal error).</p> + * + * <p>Once a session is closed, all methods on it will throw an IllegalStateException, and + * any repeating requests or bursts are stopped (as if {@link #stopRepeating()} was called). + * However, any in-progress capture requests submitted to the session will be completed + * as normal.</p> + */ + public void onClosed(CameraCaptureSession session) { + // default empty implementation + } + } + + /** + * <p>A listener for tracking the progress of a {@link CaptureRequest} + * submitted to the camera device.</p> + * + * <p>This listener is called when a request triggers a capture to start, + * and when the capture is complete. In case on an error capturing an image, + * the error method is triggered instead of the completion method.</p> + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + 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. + * + * <p>This callback is invoked right as the capture of a frame begins, + * so it is the most appropriate time for playing a shutter sound, + * or triggering UI indicators of capture.</p> + * + * <p>The request that is being used for this capture is provided, along + * with the actual timestamp for the start of exposure. This timestamp + * matches the timestamp that will be included in + * {@link CaptureResult#SENSOR_TIMESTAMP the result timestamp field}, + * and in the buffers sent to each output Surface. These buffer + * timestamps are accessible through, for example, + * {@link android.media.Image#getTimestamp() Image.getTimestamp()} or + * {@link android.graphics.SurfaceTexture#getTimestamp()}.</p> + * + * <p>For the simplest way to play a shutter sound camera shutter or a + * video recording start/stop sound, see the + * {@link android.media.MediaActionSound} class.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera the CameraDevice sending the callback + * @param request the request for the capture that just begun + * @param timestamp the timestamp at start of capture, in nanoseconds. + * + * @see android.media.MediaActionSound + */ + public void onCaptureStarted(CameraDevice camera, + CaptureRequest request, long timestamp) { + // default empty implementation + } + + /** + * This method is called when some results from an image capture are + * available. + * + * <p>The result provided here will contain some subset of the fields of + * a full result. Multiple onCapturePartial calls may happen per + * capture; a given result field will only be present in one partial + * capture at most. The final onCaptureCompleted call will always + * contain all the fields, whether onCapturePartial was called or + * not.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera The CameraDevice sending the callback. + * @param request The request that was given to the CameraDevice + * @param result The partial output metadata from the capture, which + * includes a subset of the CaptureResult fields. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + * + * @hide + */ + public void onCapturePartial(CameraDevice camera, + CaptureRequest request, CaptureResult result) { + // default empty implementation + } + + /** + * This method is called when an image capture makes partial forward progress; some + * (but not all) results from an image capture are available. + * + * <p>The result provided here will contain some subset of the fields of + * a full result. Multiple {@link #onCaptureProgressed} calls may happen per + * capture; a given result field will only be present in one partial + * capture at most. The final {@link #onCaptureCompleted} call will always + * contain all the fields (in particular, the union of all the fields of all + * the partial results composing the total result).</p> + * + * <p>For each request, some result data might be available earlier than others. The typical + * delay between each partial result (per request) is a single frame interval. + * For performance-oriented use-cases, applications should query the metadata they need + * to make forward progress from the partial results and avoid waiting for the completed + * result.</p> + * + * <p>Each request will generate at least {@code 1} partial results, and at most + * {@link CameraCharacteristics#REQUEST_PARTIAL_RESULT_COUNT} partial results.</p> + * + * <p>Depending on the request settings, the number of partial results per request + * will vary, although typically the partial count could be the same as long as the + * camera device subsystems enabled stay the same.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera The CameraDevice sending the callback. + * @param request The request that was given to the CameraDevice + * @param partialResult The partial output metadata from the capture, which + * includes a subset of the {@link TotalCaptureResult} fields. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public void onCaptureProgressed(CameraDevice camera, + CaptureRequest request, CaptureResult partialResult) { + // default empty implementation + } + + /** + * This method is called when an image capture has fully completed and all the + * result metadata is available. + * + * <p>This callback will always fire after the last {@link #onCaptureProgressed}; + * in other words, no more partial results will be delivered once the completed result + * is available.</p> + * + * <p>For performance-intensive use-cases where latency is a factor, consider + * using {@link #onCaptureProgressed} instead.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera The CameraDevice sending the callback. + * @param request The request that was given to the CameraDevice + * @param result The total output metadata from the capture, including the + * final capture parameters and the state of the camera system during + * capture. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public void onCaptureCompleted(CameraDevice camera, + CaptureRequest request, TotalCaptureResult result) { + // default empty implementation + } + + /** + * This method is called instead of {@link #onCaptureCompleted} when the + * camera device failed to produce a {@link CaptureResult} for the + * request. + * + * <p>Other requests are unaffected, and some or all image buffers from + * the capture may have been pushed to their respective output + * streams.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera + * The CameraDevice sending the callback. + * @param request + * The request that was given to the CameraDevice + * @param failure + * The output failure from the capture, including the failure reason + * and the frame number. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public void onCaptureFailed(CameraDevice camera, + CaptureRequest request, CaptureFailure failure) { + // default empty implementation + } + + /** + * This method is called independently of the others in CaptureListener, + * when a capture sequence finishes and all {@link CaptureResult} + * or {@link CaptureFailure} for it have been returned via this listener. + * + * <p>In total, there will be at least one result/failure returned by this listener + * before this callback is invoked. If the capture sequence is aborted before any + * requests have been processed, {@link #onCaptureSequenceAborted} is invoked instead.</p> + * + * <p>The default implementation does nothing.</p> + * + * @param camera + * The CameraDevice sending the callback. + * @param sequenceId + * A sequence ID returned by the {@link #capture} family of functions. + * @param frameNumber + * The last frame number (returned by {@link CaptureResult#getFrameNumber} + * or {@link CaptureFailure#getFrameNumber}) in the capture sequence. + * + * @see CaptureResult#getFrameNumber() + * @see CaptureFailure#getFrameNumber() + * @see CaptureResult#getSequenceId() + * @see CaptureFailure#getSequenceId() + * @see #onCaptureSequenceAborted + */ + public void onCaptureSequenceCompleted(CameraDevice camera, + int sequenceId, long frameNumber) { + // default empty implementation + } + + /** + * This method is called independently of the others in CaptureListener, + * when a capture sequence aborts before any {@link CaptureResult} + * or {@link CaptureFailure} for it have been returned via this listener. + * + * <p>Due to the asynchronous nature of the camera device, not all submitted captures + * are immediately processed. It is possible to clear out the pending requests + * by a variety of operations such as {@link CameraDevice#stopRepeating} or + * {@link CameraDevice#flush}. When such an event happens, + * {@link #onCaptureSequenceCompleted} will not be called.</p> + * + * <p>The default implementation does nothing.</p> + * + * @param camera + * The CameraDevice sending the callback. + * @param sequenceId + * A sequence ID returned by the {@link #capture} family of functions. + * + * @see CaptureResult#getFrameNumber() + * @see CaptureFailure#getFrameNumber() + * @see CaptureResult#getSequenceId() + * @see CaptureFailure#getSequenceId() + * @see #onCaptureSequenceCompleted + */ + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { + // default empty implementation + } + } + +} diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 5f2af8c..2f5b4fe 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -16,7 +16,10 @@ package android.hardware.camera2; +import android.hardware.camera2.CaptureResult.Key; import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.TypeReference; +import android.util.Rational; import java.util.Collections; import java.util.List; @@ -29,29 +32,173 @@ import java.util.List; * through the {@link CameraManager CameraManager} * interface in addition to through the CameraDevice interface.</p> * + * <p>{@link CameraCharacteristics} objects are immutable.</p> + * * @see CameraDevice * @see CameraManager */ -public final class CameraCharacteristics extends CameraMetadata { +public final class CameraCharacteristics extends CameraMetadata<CameraCharacteristics.Key<?>> { + + /** + * A {@code Key} is used to do camera characteristics field lookups with + * {@link CameraCharacteristics#get}. + * + * <p>For example, to get the stream configuration map: + * <code><pre> + * StreamConfigurationMap map = cameraCharacteristics.get( + * CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + * </pre></code> + * </p> + * + * <p>To enumerate over all possible keys for {@link CameraCharacteristics}, see + * {@link CameraCharacteristics#getKeys()}.</p> + * + * @see CameraCharacteristics#get + * @see CameraCharacteristics#getKeys() + */ + public static final class Key<T> { + private final CameraMetadataNative.Key<T> mKey; + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, Class<T> type) { + mKey = new CameraMetadataNative.Key<T>(name, type); + } + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, TypeReference<T> typeReference) { + mKey = new CameraMetadataNative.Key<T>(name, typeReference); + } + + /** + * Return a camelCase, period separated name formatted like: + * {@code "root.section[.subsections].name"}. + * + * <p>Built-in keys exposed by the Android SDK are always prefixed with {@code "android."}; + * keys that are device/platform-specific are prefixed with {@code "com."}.</p> + * + * <p>For example, {@code CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP} would + * have a name of {@code "android.scaler.streamConfigurationMap"}; whereas a device + * specific key might look like {@code "com.google.nexus.data.private"}.</p> + * + * @return String representation of the key name + */ + public String getName() { + return mKey.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public final int hashCode() { + return mKey.hashCode(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public final boolean equals(Object o) { + return o instanceof Key && ((Key<T>)o).mKey.equals(mKey); + } + + /** + * Visible for CameraMetadataNative implementation only; do not use. + * + * TODO: Make this private or remove it altogether. + * + * @hide + */ + public CameraMetadataNative.Key<T> getNativeKey() { + return mKey; + } + + @SuppressWarnings({ + "unused", "unchecked" + }) + private Key(CameraMetadataNative.Key<?> nativeKey) { + mKey = (CameraMetadataNative.Key<T>) nativeKey; + } + } private final CameraMetadataNative mProperties; - private List<Key<?>> mAvailableRequestKeys; - private List<Key<?>> mAvailableResultKeys; + private List<CaptureRequest.Key<?>> mAvailableRequestKeys; + private List<CaptureResult.Key<?>> mAvailableResultKeys; /** * Takes ownership of the passed-in properties object * @hide */ public CameraCharacteristics(CameraMetadataNative properties) { - mProperties = properties; + mProperties = CameraMetadataNative.move(properties); } - @Override + /** + * Returns a copy of the underlying {@link CameraMetadataNative}. + * @hide + */ + public CameraMetadataNative getNativeCopy() { + return new CameraMetadataNative(mProperties); + } + + /** + * Get a camera characteristics field value. + * + * <p>The field definitions can be + * found in {@link CameraCharacteristics}.</p> + * + * <p>Querying the value for the same key more than once will return a value + * which is equal to the previous queried value.</p> + * + * @throws IllegalArgumentException if the key was not valid + * + * @param key The characteristics field to read. + * @return The value of that key, or {@code null} if the field is not set. + */ public <T> T get(Key<T> key) { return mProperties.get(key); } /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected <T> T getProtected(Key<?> key) { + return (T) mProperties.get(key); + } + + /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected Class<Key<?>> getKeyClass() { + Object thisClass = Key.class; + return (Class<Key<?>>)thisClass; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Key<?>> getKeys() { + // Force the javadoc for this function to show up on the CameraCharacteristics page + return super.getKeys(); + } + + /** * Returns the list of keys supported by this {@link CameraDevice} for querying * with a {@link CaptureRequest}. * @@ -65,9 +212,14 @@ public final class CameraCharacteristics extends CameraMetadata { * * @return List of keys supported by this CameraDevice for CaptureRequests. */ - public List<Key<?>> getAvailableCaptureRequestKeys() { + @SuppressWarnings({"unchecked"}) + public List<CaptureRequest.Key<?>> getAvailableCaptureRequestKeys() { if (mAvailableRequestKeys == null) { - mAvailableRequestKeys = getAvailableKeyList(CaptureRequest.class); + Object crKey = CaptureRequest.Key.class; + Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>)crKey; + + mAvailableRequestKeys = Collections.unmodifiableList( + getAvailableKeyList(CaptureRequest.class, crKeyTyped)); } return mAvailableRequestKeys; } @@ -86,9 +238,14 @@ public final class CameraCharacteristics extends CameraMetadata { * * @return List of keys supported by this CameraDevice for CaptureResults. */ - public List<Key<?>> getAvailableCaptureResultKeys() { + @SuppressWarnings({"unchecked"}) + public List<CaptureResult.Key<?>> getAvailableCaptureResultKeys() { if (mAvailableResultKeys == null) { - mAvailableResultKeys = getAvailableKeyList(CaptureResult.class); + Object crKey = CaptureResult.Key.class; + Class<CaptureResult.Key<?>> crKeyTyped = (Class<CaptureResult.Key<?>>)crKey; + + mAvailableResultKeys = Collections.unmodifiableList( + getAvailableKeyList(CaptureResult.class, crKeyTyped)); } return mAvailableResultKeys; } @@ -102,12 +259,14 @@ public final class CameraCharacteristics extends CameraMetadata { * <p>Each key is only listed once in the list. The order of the keys is undefined.</p> * * @param metadataClass The subclass of CameraMetadata that you want to get the keys for. + * @param keyClass The class of the metadata key, e.g. CaptureRequest.Key.class * * @return List of keys supported by this CameraDevice for metadataClass. * * @throws IllegalArgumentException if metadataClass is not a subclass of CameraMetadata */ - private <T extends CameraMetadata> List<Key<?>> getAvailableKeyList(Class<T> metadataClass) { + private <TKey> List<TKey> + getAvailableKeyList(Class<?> metadataClass, Class<TKey> keyClass) { if (metadataClass.equals(CameraMetadata.class)) { throw new AssertionError( @@ -117,7 +276,9 @@ public final class CameraCharacteristics extends CameraMetadata { "metadataClass must be a subclass of CameraMetadata"); } - return Collections.unmodifiableList(getKeysStatic(metadataClass, /*instance*/null)); + List<TKey> staticKeyList = CameraCharacteristics.<TKey>getKeysStatic( + metadataClass, keyClass, /*instance*/null); + return Collections.unmodifiableList(staticKeyList); } /*@O~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ @@ -135,8 +296,8 @@ public final class CameraCharacteristics extends CameraMetadata { * 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); + public static final Key<int[]> CONTROL_AE_AVAILABLE_ANTIBANDING_MODES = + new Key<int[]>("android.control.aeAvailableAntibandingModes", int[].class); /** * <p>The set of auto-exposure modes that are supported by this @@ -154,15 +315,15 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#CONTROL_AE_MODE */ - public static final Key<byte[]> CONTROL_AE_AVAILABLE_MODES = - new Key<byte[]>("android.control.aeAvailableModes", byte[].class); + public static final Key<int[]> CONTROL_AE_AVAILABLE_MODES = + new Key<int[]>("android.control.aeAvailableModes", int[].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); + public static final Key<android.util.Range<Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES = + new Key<android.util.Range<Integer>[]>("android.control.aeAvailableTargetFpsRanges", new TypeReference<android.util.Range<Integer>[]>() {{ }}); /** * <p>Maximum and minimum exposure compensation @@ -171,8 +332,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP */ - public static final Key<int[]> CONTROL_AE_COMPENSATION_RANGE = - new Key<int[]>("android.control.aeCompensationRange", int[].class); + public static final Key<android.util.Range<Integer>> CONTROL_AE_COMPENSATION_RANGE = + new Key<android.util.Range<Integer>>("android.control.aeCompensationRange", new TypeReference<android.util.Range<Integer>>() {{ }}); /** * <p>Smallest step by which exposure compensation @@ -194,8 +355,8 @@ public final class CameraCharacteristics extends CameraMetadata { * @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); + public static final Key<int[]> CONTROL_AF_AVAILABLE_MODES = + new Key<int[]>("android.control.afAvailableModes", int[].class); /** * <p>List containing the subset of color effects @@ -213,8 +374,8 @@ public final class CameraCharacteristics extends CameraMetadata { * @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); + public static final Key<int[]> CONTROL_AVAILABLE_EFFECTS = + new Key<int[]>("android.control.availableEffects", int[].class); /** * <p>List containing a subset of scene modes @@ -227,15 +388,15 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#CONTROL_SCENE_MODE */ - public static final Key<byte[]> CONTROL_AVAILABLE_SCENE_MODES = - new Key<byte[]>("android.control.availableSceneModes", byte[].class); + public static final Key<int[]> CONTROL_AVAILABLE_SCENE_MODES = + new Key<int[]>("android.control.availableSceneModes", int[].class); /** * <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); + public static final Key<int[]> CONTROL_AVAILABLE_VIDEO_STABILIZATION_MODES = + new Key<int[]>("android.control.availableVideoStabilizationModes", int[].class); /** * <p>The set of auto-white-balance modes ({@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode}) @@ -253,8 +414,8 @@ public final class CameraCharacteristics extends CameraMetadata { * @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); + public static final Key<int[]> CONTROL_AWB_AVAILABLE_MODES = + new Key<int[]>("android.control.awbAvailableModes", int[].class); /** * <p>List of the maximum number of regions that can be used for metering in @@ -266,19 +427,53 @@ public final class CameraCharacteristics extends CameraMetadata { * @see CaptureRequest#CONTROL_AE_REGIONS * @see CaptureRequest#CONTROL_AF_REGIONS * @see CaptureRequest#CONTROL_AWB_REGIONS + * @hide */ public static final Key<int[]> CONTROL_MAX_REGIONS = new Key<int[]>("android.control.maxRegions", int[].class); /** + * <p>List of the maximum number of regions that can be used for metering in + * auto-exposure (AE); + * this corresponds to the the maximum number of elements in + * {@link CaptureRequest#CONTROL_AE_REGIONS android.control.aeRegions}.</p> + * + * @see CaptureRequest#CONTROL_AE_REGIONS + */ + public static final Key<Integer> CONTROL_MAX_REGIONS_AE = + new Key<Integer>("android.control.maxRegionsAe", int.class); + + /** + * <p>List of the maximum number of regions that can be used for metering in + * auto-white balance (AWB); + * this corresponds to the the maximum number of elements in + * {@link CaptureRequest#CONTROL_AWB_REGIONS android.control.awbRegions}.</p> + * + * @see CaptureRequest#CONTROL_AWB_REGIONS + */ + public static final Key<Integer> CONTROL_MAX_REGIONS_AWB = + new Key<Integer>("android.control.maxRegionsAwb", int.class); + + /** + * <p>List of the maximum number of regions that can be used for metering in + * auto-focus (AF); + * this corresponds to the the maximum number of elements in + * {@link CaptureRequest#CONTROL_AF_REGIONS android.control.afRegions}.</p> + * + * @see CaptureRequest#CONTROL_AF_REGIONS + */ + public static final Key<Integer> CONTROL_MAX_REGIONS_AF = + new Key<Integer>("android.control.maxRegionsAf", int.class); + + /** * <p>The set of edge enhancement modes supported by this camera device.</p> * <p>This tag lists the valid modes for {@link CaptureRequest#EDGE_MODE android.edge.mode}.</p> * <p>Full-capability camera devices must always support OFF and FAST.</p> * * @see CaptureRequest#EDGE_MODE */ - public static final Key<byte[]> EDGE_AVAILABLE_EDGE_MODES = - new Key<byte[]>("android.edge.availableEdgeModes", byte[].class); + public static final Key<int[]> EDGE_AVAILABLE_EDGE_MODES = + new Key<int[]>("android.edge.availableEdgeModes", int[].class); /** * <p>Whether this camera device has a @@ -297,8 +492,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#HOT_PIXEL_MODE */ - public static final Key<byte[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES = - new Key<byte[]>("android.hotPixel.availableHotPixelModes", byte[].class); + public static final Key<int[]> HOT_PIXEL_AVAILABLE_HOT_PIXEL_MODES = + new Key<int[]>("android.hotPixel.availableHotPixelModes", int[].class); /** * <p>Supported resolutions for the JPEG thumbnail</p> @@ -307,19 +502,17 @@ public final class CameraCharacteristics extends CameraMetadata { * <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}. + * aspect ratio of largest JPEG output size in 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 + * <li>Each output JPEG size in 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); + public static final Key<android.util.Size[]> JPEG_AVAILABLE_THUMBNAIL_SIZES = + new Key<android.util.Size[]>("android.jpeg.availableThumbnailSizes", android.util.Size[].class); /** * <p>List of supported aperture @@ -367,8 +560,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @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); + public static final Key<int[]> LENS_INFO_AVAILABLE_OPTICAL_STABILIZATION = + new Key<int[]>("android.lens.info.availableOpticalStabilization", int[].class); /** * <p>Optional. Hyperfocal distance for this lens.</p> @@ -394,9 +587,10 @@ public final class CameraCharacteristics extends CameraMetadata { * <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> + * @hide */ - 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); + public static final Key<android.util.Size> LENS_INFO_SHADING_MAP_SIZE = + new Key<android.util.Size>("android.lens.info.shadingMapSize", android.util.Size.class); /** * <p>The lens focus distance calibration quality.</p> @@ -432,8 +626,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#NOISE_REDUCTION_MODE */ - public static final Key<byte[]> NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES = - new Key<byte[]>("android.noiseReduction.availableNoiseReductionModes", byte[].class); + public static final Key<int[]> NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES = + new Key<int[]>("android.noiseReduction.availableNoiseReductionModes", int[].class); /** * <p>If set to 1, the HAL will always split result @@ -445,8 +639,10 @@ public final class CameraCharacteristics extends CameraMetadata { * working at that point; DO NOT USE without careful * consideration of future support.</p> * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> + * @deprecated * @hide */ + @Deprecated public static final Key<Byte> QUIRKS_USE_PARTIAL_RESULT = new Key<Byte>("android.quirks.usePartialResult", byte.class); @@ -460,9 +656,9 @@ public final class CameraCharacteristics extends CameraMetadata { * 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 + * CPU resources that will consume more power. The image format for an output stream can + * be any supported format provided by android.scaler.availableStreamConfigurations. + * The formats defined in 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. @@ -471,26 +667,90 @@ public final class CameraCharacteristics extends CameraMetadata { * <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 + * @hide */ public static final Key<int[]> REQUEST_MAX_NUM_OUTPUT_STREAMS = new Key<int[]>("android.request.maxNumOutputStreams", int[].class); /** + * <p>The maximum numbers of different types of output streams + * that can be configured and used simultaneously by a camera device + * for any <code>RAW</code> formats.</p> + * <p>This value contains the max number of output simultaneous + * streams from the raw sensor.</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 this kind of an output stream can + * be any <code>RAW</code> and supported format provided by {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}.</p> + * <p>In particular, a <code>RAW</code> format is typically one of:</p> + * <ul> + * <li>ImageFormat#RAW_SENSOR</li> + * <li>Opaque <code>RAW</code></li> + * </ul> + * + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP + */ + public static final Key<Integer> REQUEST_MAX_NUM_OUTPUT_RAW = + new Key<Integer>("android.request.maxNumOutputRaw", int.class); + + /** + * <p>The maximum numbers of different types of output streams + * that can be configured and used simultaneously by a camera device + * for any processed (but not-stalling) formats.</p> + * <p>This value contains the max number of output simultaneous + * streams for any processed (but not-stalling) formats.</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 this kind of an output stream can + * be any non-<code>RAW</code> and supported format provided by {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}.</p> + * <p>Processed (but not-stalling) is defined as any non-RAW format without a stall duration. + * Typically:</p> + * <ul> + * <li>ImageFormat#YUV_420_888</li> + * <li>ImageFormat#NV21</li> + * <li>ImageFormat#YV12</li> + * <li>Implementation-defined formats, i.e. StreamConfiguration#isOutputSupportedFor(Class)</li> + * </ul> + * <p>For full guarantees, query StreamConfigurationMap#getOutputStallDuration with + * a processed format -- it will return 0 for a non-stalling stream.</p> + * + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP + */ + public static final Key<Integer> REQUEST_MAX_NUM_OUTPUT_PROC = + new Key<Integer>("android.request.maxNumOutputProc", int.class); + + /** + * <p>The maximum numbers of different types of output streams + * that can be configured and used simultaneously by a camera device + * for any processed (and stalling) formats.</p> + * <p>This value contains the max number of output simultaneous + * streams for any processed (but not-stalling) formats.</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 this kind of an output stream can + * be any non-<code>RAW</code> and supported format provided by {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}.</p> + * <p>A processed and stalling format is defined as any non-RAW format with a stallDurations > 0. + * Typically only the <code>JPEG</code> format (ImageFormat#JPEG)</p> + * <p>For full guarantees, query StreamConfigurationMap#getOutputStallDuration with + * a processed format -- it will return a non-0 value for a stalling stream.</p> + * + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP + */ + public static final Key<Integer> REQUEST_MAX_NUM_OUTPUT_PROC_STALLING = + new Key<Integer>("android.request.maxNumOutputProcStalling", int.class); + + /** * <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 + * 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); @@ -550,7 +810,6 @@ public final class CameraCharacteristics extends CameraMetadata { * {@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> @@ -559,12 +818,12 @@ public final class CameraCharacteristics extends CameraMetadata { * @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_MANUAL_POST_PROCESSING * @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); + public static final Key<int[]> REQUEST_AVAILABLE_CAPABILITIES = + new Key<int[]>("android.request.availableCapabilities", int[].class); /** * <p>A list of all keys that the camera device has available @@ -593,7 +852,7 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * <li>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> @@ -604,7 +863,6 @@ public final class CameraCharacteristics extends CameraMetadata { * <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 */ @@ -629,22 +887,26 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * @deprecated + * @hide */ + @Deprecated 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 {@link CameraCharacteristics#SCALER_AVAILABLE_JPEG_SIZES android.scaler.availableJpegSizes}.</p> + * for each resolution in 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 + * @deprecated + * @hide */ + @Deprecated public static final Key<long[]> SCALER_AVAILABLE_JPEG_MIN_DURATIONS = new Key<long[]>("android.scaler.availableJpegMinDurations", long[].class); @@ -654,9 +916,12 @@ public final class CameraCharacteristics extends CameraMetadata { * sensor maximum resolution (defined by {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}).</p> * * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE + * @deprecated + * @hide */ - 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); + @Deprecated + public static final Key<android.util.Size[]> SCALER_AVAILABLE_JPEG_SIZES = + new Key<android.util.Size[]>("android.scaler.availableJpegSizes", android.util.Size[].class); /** * <p>The maximum ratio between active area width @@ -669,16 +934,17 @@ public final class CameraCharacteristics extends CameraMetadata { /** * <p>For each available processed output size (defined in - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES android.scaler.availableProcessedSizes}), this property lists the + * 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 + * @deprecated + * @hide */ + @Deprecated public static final Key<long[]> SCALER_AVAILABLE_PROCESSED_MIN_DURATIONS = new Key<long[]>("android.scaler.availableProcessedMinDurations", long[].class); @@ -696,9 +962,12 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * @deprecated + * @hide */ - 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); + @Deprecated + public static final Key<android.util.Size[]> SCALER_AVAILABLE_PROCESSED_SIZES = + new Key<android.util.Size[]>("android.scaler.availableProcessedSizes", android.util.Size[].class); /** * <p>The mapping of image formats that are supported by this @@ -746,13 +1015,14 @@ public final class CameraCharacteristics extends CameraMetadata { * </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> + * StreamConfigurationMap#getOutputStallDuration(int,Size) + * for a <code>format =</code> RAW_OPAQUE is always 0).</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> + * <p>TODO: typedef to ReprocessFormatMap</p> * * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + * @hide */ public static final Key<int[]> SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP = new Key<int[]>("android.scaler.availableInputOutputFormatsMap", int[].class); @@ -775,7 +1045,7 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * 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> @@ -844,13 +1114,11 @@ public final class CameraCharacteristics extends CameraMetadata { * * @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 + * @hide */ - public static final Key<int[]> SCALER_AVAILABLE_STREAM_CONFIGURATIONS = - new Key<int[]>("android.scaler.availableStreamConfigurations", int[].class); + public static final Key<android.hardware.camera2.params.StreamConfiguration[]> SCALER_AVAILABLE_STREAM_CONFIGURATIONS = + new Key<android.hardware.camera2.params.StreamConfiguration[]>("android.scaler.availableStreamConfigurations", android.hardware.camera2.params.StreamConfiguration[].class); /** * <p>This lists the minimum frame duration for each @@ -863,14 +1131,16 @@ public final class CameraCharacteristics extends CameraMetadata { * <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 + * android.scaler.availableStallDurations for more details about * calculating the max frame rate.</p> + * <p>(Keep in sync with + * StreamConfigurationMap#getOutputMinFrameDuration)</p> * - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS * @see CaptureRequest#SENSOR_FRAME_DURATION + * @hide */ - public static final Key<long[]> SCALER_AVAILABLE_MIN_FRAME_DURATIONS = - new Key<long[]>("android.scaler.availableMinFrameDurations", long[].class); + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> SCALER_AVAILABLE_MIN_FRAME_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.scaler.availableMinFrameDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); /** * <p>This lists the maximum stall duration for each @@ -929,12 +1199,124 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * <p>(Keep up to date with + * StreamConfigurationMap#getOutputStallDuration(int, Size) )</p> * * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES * @see CaptureRequest#SENSOR_FRAME_DURATION + * @hide */ - public static final Key<long[]> SCALER_AVAILABLE_STALL_DURATIONS = - new Key<long[]>("android.scaler.availableStallDurations", long[].class); + public static final Key<android.hardware.camera2.params.StreamConfigurationDuration[]> SCALER_AVAILABLE_STALL_DURATIONS = + new Key<android.hardware.camera2.params.StreamConfigurationDuration[]>("android.scaler.availableStallDurations", android.hardware.camera2.params.StreamConfigurationDuration[].class); + + /** + * <p>The available stream configurations that this + * camera device supports; also includes the minimum frame durations + * and the stall durations for each format/size combination.</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>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#SENSOR_INFO_ACTIVE_ARRAY_SIZE + */ + public static final Key<android.hardware.camera2.params.StreamConfigurationMap> SCALER_STREAM_CONFIGURATION_MAP = + new Key<android.hardware.camera2.params.StreamConfigurationMap>("android.scaler.streamConfigurationMap", android.hardware.camera2.params.StreamConfigurationMap.class); + + /** + * <p>The crop type that this camera device supports.</p> + * <p>When passing a non-centered crop region ({@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}) to a camera + * device that only supports CENTER_ONLY cropping, the camera device will move the + * crop region to the center of the sensor active array ({@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}) + * and keep the crop region width and height unchanged. The camera device will return the + * final used crop region in metadata result {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}.</p> + * <p>Camera devices that support FREEFORM cropping will support any crop region that + * is inside of the active array. The camera device will apply the same crop region and + * return the final used crop region in capture result metadata {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion}.</p> + * <p>FULL capability devices ({@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} <code>==</code> FULL) will support + * FREEFORM cropping.</p> + * + * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL + * @see CaptureRequest#SCALER_CROP_REGION + * @see CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE + * @see #SCALER_CROPPING_TYPE_CENTER_ONLY + * @see #SCALER_CROPPING_TYPE_FREEFORM + */ + public static final Key<Integer> SCALER_CROPPING_TYPE = + new Key<Integer>("android.scaler.croppingType", int.class); /** * <p>Area of raw data which corresponds to only @@ -948,8 +1330,8 @@ public final class CameraCharacteristics extends CameraMetadata { /** * <p>Range of valid sensitivities</p> */ - public static final Key<int[]> SENSOR_INFO_SENSITIVITY_RANGE = - new Key<int[]>("android.sensor.info.sensitivityRange", int[].class); + public static final Key<android.util.Range<Integer>> SENSOR_INFO_SENSITIVITY_RANGE = + new Key<android.util.Range<Integer>>("android.sensor.info.sensitivityRange", new TypeReference<android.util.Range<Integer>>() {{ }}); /** * <p>Arrangement of color filters on sensor; @@ -970,8 +1352,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#SENSOR_EXPOSURE_TIME */ - public static final Key<long[]> SENSOR_INFO_EXPOSURE_TIME_RANGE = - new Key<long[]>("android.sensor.info.exposureTimeRange", long[].class); + public static final Key<android.util.Range<Long>> SENSOR_INFO_EXPOSURE_TIME_RANGE = + new Key<android.util.Range<Long>>("android.sensor.info.exposureTimeRange", new TypeReference<android.util.Range<Long>>() {{ }}); /** * <p>Maximum possible frame duration (minimum frame @@ -982,13 +1364,9 @@ public final class CameraCharacteristics extends CameraMetadata { * 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> + * StreamConfigurationMap#getOutputMinFrameDuration(int,Size) + * 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 = @@ -999,20 +1377,18 @@ public final class CameraCharacteristics extends CameraMetadata { * 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); + public static final Key<android.util.SizeF> SENSOR_INFO_PHYSICAL_SIZE = + new Key<android.util.SizeF>("android.sensor.info.physicalSize", android.util.SizeF.class); /** * <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 + * android.scaler.availableStreamConfigurations.</p> */ - 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); + public static final Key<android.util.Size> SENSOR_INFO_PIXEL_ARRAY_SIZE = + new Key<android.util.Size>("android.sensor.info.pixelArraySize", android.util.Size.class); /** * <p>Maximum raw value output by sensor.</p> @@ -1108,8 +1484,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 */ - public static final Key<Rational[]> SENSOR_CALIBRATION_TRANSFORM1 = - new Key<Rational[]>("android.sensor.calibrationTransform1", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_CALIBRATION_TRANSFORM1 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.calibrationTransform1", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A per-device calibration transform matrix that maps from the @@ -1129,8 +1505,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 */ - public static final Key<Rational[]> SENSOR_CALIBRATION_TRANSFORM2 = - new Key<Rational[]>("android.sensor.calibrationTransform2", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_CALIBRATION_TRANSFORM2 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.calibrationTransform2", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A matrix that transforms color values from CIE XYZ color space to @@ -1151,8 +1527,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 */ - public static final Key<Rational[]> SENSOR_COLOR_TRANSFORM1 = - new Key<Rational[]>("android.sensor.colorTransform1", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_COLOR_TRANSFORM1 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.colorTransform1", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A matrix that transforms color values from CIE XYZ color space to @@ -1175,8 +1551,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT2 */ - public static final Key<Rational[]> SENSOR_COLOR_TRANSFORM2 = - new Key<Rational[]>("android.sensor.colorTransform2", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_COLOR_TRANSFORM2 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.colorTransform2", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A matrix that transforms white balanced camera colors from the reference @@ -1195,8 +1571,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CameraCharacteristics#SENSOR_REFERENCE_ILLUMINANT1 */ - public static final Key<Rational[]> SENSOR_FORWARD_MATRIX1 = - new Key<Rational[]>("android.sensor.forwardMatrix1", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_FORWARD_MATRIX1 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.forwardMatrix1", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A matrix that transforms white balanced camera colors from the reference @@ -1217,21 +1593,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @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> - * - * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL - */ - public static final Key<Rational> SENSOR_BASE_GAIN_FACTOR = - new Key<Rational>("android.sensor.baseGainFactor", Rational.class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> SENSOR_FORWARD_MATRIX2 = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.sensor.forwardMatrix2", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <p>A fixed black level offset for each of the color filter arrangement @@ -1298,8 +1661,8 @@ public final class CameraCharacteristics extends CameraMetadata { * android.statistics.faceIds and * 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); + public static final Key<int[]> STATISTICS_INFO_AVAILABLE_FACE_DETECT_MODES = + new Key<int[]>("android.statistics.info.availableFaceDetectModes", int[].class); /** * <p>Maximum number of simultaneously detectable @@ -1322,19 +1685,16 @@ public final class CameraCharacteristics extends CameraMetadata { /** * <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> + * tonemap curve that can be used for {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}.</p> * <p>If the actual number of points provided by the application (in - * android.tonemap.curve*) is less than max, the camera device will + * {@link CaptureRequest#TONEMAP_CURVE 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 + * @see CaptureRequest#TONEMAP_CURVE */ public static final Key<Integer> TONEMAP_MAX_CURVE_POINTS = new Key<Integer>("android.tonemap.maxCurvePoints", int.class); @@ -1347,8 +1707,8 @@ public final class CameraCharacteristics extends CameraMetadata { * * @see CaptureRequest#TONEMAP_MODE */ - public static final Key<byte[]> TONEMAP_AVAILABLE_TONE_MAP_MODES = - new Key<byte[]>("android.tonemap.availableToneMapModes", byte[].class); + public static final Key<int[]> TONEMAP_AVAILABLE_TONE_MAP_MODES = + new Key<int[]>("android.tonemap.availableToneMapModes", int[].class); /** * <p>A list of camera LEDs that are available on this system.</p> @@ -1364,15 +1724,16 @@ public final class CameraCharacteristics extends CameraMetadata { * <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> + * <li>30fps at maximum resolution (== sensor resolution) is preferred, more than 20fps is required.</li> + * <li>Per frame control ({@link CameraCharacteristics#SYNC_MAX_LATENCY android.sync.maxLatency} <code>==</code> PER_FRAME_CONTROL)</li> + * <li>Manual sensor control ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_SENSOR)</li> + * <li>Manual post-processing control ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_POST_PROCESSING)</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 CameraCharacteristics#SYNC_MAX_LATENCY * @see #INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED * @see #INFO_SUPPORTED_HARDWARE_LEVEL_FULL */ diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java index 9d0e0e1..6f5099b 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -16,6 +16,8 @@ package android.hardware.camera2; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.graphics.ImageFormat; import android.os.Handler; import android.view.Surface; @@ -147,7 +149,7 @@ public interface CameraDevice extends AutoCloseable { * the size of the Surface with * {@link android.view.SurfaceHolder#setFixedSize} to be one of the * supported - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES processed sizes} + * {@link StreamConfigurationMap#getOutputSizes(Class) processed sizes} * before calling {@link android.view.SurfaceHolder#getSurface}.</li> * * <li>For accessing through an OpenGL texture via a @@ -155,14 +157,14 @@ public interface CameraDevice extends AutoCloseable { * the SurfaceTexture with * {@link android.graphics.SurfaceTexture#setDefaultBufferSize} to be one * of the supported - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES processed sizes} + * {@link StreamConfigurationMap#getOutputSizes(Class) processed sizes} * before creating a Surface from the SurfaceTexture with * {@link Surface#Surface}.</li> * * <li>For recording with {@link android.media.MediaCodec}: Call * {@link android.media.MediaCodec#createInputSurface} after configuring * the media codec to use one of the - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES processed sizes} + * {@link StreamConfigurationMap#getOutputSizes(Class) processed sizes} * </li> * * <li>For recording with {@link android.media.MediaRecorder}: TODO</li> @@ -171,18 +173,15 @@ public interface CameraDevice extends AutoCloseable { * Create a RenderScript * {@link android.renderscript.Allocation Allocation} with a supported YUV * type, the IO_INPUT flag, and one of the supported - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES processed sizes}. Then + * {@link StreamConfigurationMap#getOutputSizes(int) processed sizes}. Then * obtain the Surface with * {@link android.renderscript.Allocation#getSurface}.</li> * - * <li>For access to uncompressed or JPEG data in the application: Create a - * {@link android.media.ImageReader} object with the desired - * {@link CameraCharacteristics#SCALER_AVAILABLE_FORMATS image format}, and a - * size from the matching - * {@link CameraCharacteristics#SCALER_AVAILABLE_PROCESSED_SIZES processed}, - * {@link CameraCharacteristics#SCALER_AVAILABLE_JPEG_SIZES jpeg}. Then obtain - * a Surface from it.</li> - * + * <li>For access to uncompressed or {@link ImageFormat#JPEG JPEG} data in the application: + * Create a {@link android.media.ImageReader} object with the desired + * {@link StreamConfigurationMap#getOutputFormats() image format}, and a size from the matching + * {@link StreamConfigurationMap#getOutputSizes(int) processed size} and {@code format}. + * Then obtain a {@link Surface} from it.</li> * </ul> * * </p> @@ -243,10 +242,126 @@ public interface CameraDevice extends AutoCloseable { * @see StreamConfigurationMap#getOutputFormats() * @see StreamConfigurationMap#getOutputSizes(int) * @see StreamConfigurationMap#getOutputSizes(Class) + * @deprecated Use {@link #createCaptureSession} instead */ public void configureOutputs(List<Surface> outputs) throws CameraAccessException; /** + * <p>Create a new camera capture session by providing the target output set of Surfaces to the + * camera device.</p> + * + * <p>The active capture session determines the set of potential output Surfaces for + * the camera device for each capture request. A given request may use all + * or a only some of the outputs. Once the CameraCaptureSession is created, requests can be + * can be submitted with {@link CameraCaptureSession#capture capture}, + * {@link CameraCaptureSession#captureBurst captureBurst}, + * {@link CameraCaptureSession#setRepeatingRequest setRepeatingRequest}, or + * {@link CameraCaptureSession#setRepeatingBurst setRepeatingBurst}.</p> + * + * <p>Surfaces suitable for inclusion as a camera output can be created for + * various use cases and targets:</p> + * + * <ul> + * + * <li>For drawing to a {@link android.view.SurfaceView SurfaceView}: Set the size of the + * Surface with {@link android.view.SurfaceHolder#setFixedSize} to be one of the sizes + * returned by + * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(SurfaceView.class)} + * and then obtain the Surface by calling {@link android.view.SurfaceHolder#getSurface}.</li> + * + * <li>For accessing through an OpenGL texture via a + * {@link android.graphics.SurfaceTexture SurfaceTexture}: Set the size of + * the SurfaceTexture with + * {@link android.graphics.SurfaceTexture#setDefaultBufferSize} to be one + * of the sizes returned by + * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(SurfaceTexture.class)} + * before creating a Surface from the SurfaceTexture with + * {@link Surface#Surface}.</li> + * + * <li>For recording with {@link android.media.MediaCodec}: Call + * {@link android.media.MediaCodec#createInputSurface} after configuring + * the media codec to use one of the sizes returned by + * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(MediaCodec.class)} + * </li> + * + * <li>For recording with {@link android.media.MediaRecorder}: Call + * {@link android.media.MediaRecorder#getSurface} after configuring the media recorder to use + * one of the sizes returned by + * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(MediaRecorder.class)}, + * or configuring it to use one of the supported + * {@link android.media.CamcorderProfile CamcorderProfiles}.</li> + * + * <li>For efficient YUV processing with {@link android.renderscript}: + * Create a RenderScript + * {@link android.renderscript.Allocation Allocation} with a supported YUV + * type, the IO_INPUT flag, and one of the sizes returned by + * {@link StreamConfigurationMap#getOutputSizes(Class) getOutputSizes(Allocation.class)}, + * Then obtain the Surface with + * {@link android.renderscript.Allocation#getSurface}.</li> + * + * <li>For access to raw, uncompressed or JPEG data in the application: Create a + * {@link android.media.ImageReader} object with the one of the supported + * {@link StreamConfigurationMap#getOutputFormats() output image formats}, and a + * size from the supported + * {@link StreamConfigurationMap#getOutputSizes(int) sizes for that format}. Then obtain + * a Surface from it with {@link android.media.ImageReader#getSurface}.</li> + * + * </ul> + * + * </p> + * + * <p>The camera device will query each Surface's size and formats upon this + * call, so they must be set to a valid setting at this time (in particular: + * if the format is user-visible, it must be one of + * {@link StreamConfigurationMap#getOutputFormats}; and the size must be one of + * {@link StreamConfigurationMap#getOutputSizes(int)}).</p> + * + * <p>It can take several hundred milliseconds for the session's configuration to complete, + * since camera hardware may need to be powered on or reconfigured. Once the configuration is + * complete and the session is ready to actually capture data, the provided + * {@link CameraCaptureSession.StateListener}'s + * {@link CameraCaptureSession.StateListener#onConfigured} callback will be called.</p> + * + * <p>If a prior CameraCaptureSession already exists when a new one is created, the previous + * session is closed. Any in-progress capture requests made on the prior session will be + * completed before the new session is configured and is able to start capturing its own + * requests. To minimize the transition time, the {@link CameraCaptureSession#abortCaptures} + * call can be used to discard the remaining requests for the prior capture session before a new + * one is created. Note that once the new session is created, the old one can no longer have its + * captures aborted.</p> + * + * <p>Using larger resolution outputs, or more outputs, can result in slower + * output rate from the device.</p> + * + * <p>Configuring a session with an empty or null list will close the current session, if + * any. This can be used to release the current session's target surfaces for another use.</p> + * + * @param outputs The new set of Surfaces that should be made available as + * targets for captured image data. + * @param listener The listener to notify about the status of the new capture session. + * @param handler The handler on which the listener should be invoked, or {@code null} to use + * the current thread's {@link android.os.Looper looper}. + * <!-- + * @return A new camera capture session to use, or null if an empty/null set of Surfaces is + * provided. + * --> + * @throws IllegalArgumentException if the set of output Surfaces do not meet the requirements, + * the listener is null, or the handler is null but the current + * thread has no looper. + * @throws CameraAccessException if the camera device is no longer connected or has + * encountered a fatal error + * @throws IllegalStateException if the camera device has been closed + * + * @see CameraCaptureSession + * @see StreamConfigurationMap#getOutputFormats() + * @see StreamConfigurationMap#getOutputSizes(int) + * @see StreamConfigurationMap#getOutputSizes(Class) + */ + public void createCaptureSession(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler handler) + throws CameraAccessException; + + /** * <p>Create a {@link CaptureRequest.Builder} for new capture requests, * initialized with template for a target use case. The settings are chosen * to be the best options for the specific camera device, so it is not @@ -315,6 +430,7 @@ public interface CameraDevice extends AutoCloseable { * @see #captureBurst * @see #setRepeatingRequest * @see #setRepeatingBurst + * @deprecated Use {@link CameraCaptureSession} instead */ public int capture(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -359,6 +475,7 @@ public interface CameraDevice extends AutoCloseable { * @see #capture * @see #setRepeatingRequest * @see #setRepeatingBurst + * @deprecated Use {@link CameraCaptureSession} instead */ public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -417,6 +534,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingBurst * @see #stopRepeating * @see #flush + * @deprecated Use {@link CameraCaptureSession} instead */ public int setRepeatingRequest(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -475,6 +593,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingRequest * @see #stopRepeating * @see #flush + * @deprecated Use {@link CameraCaptureSession} instead */ public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; @@ -499,6 +618,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingRequest * @see #setRepeatingBurst * @see StateListener#onIdle + * @deprecated Use {@link CameraCaptureSession} instead */ public void stopRepeating() throws CameraAccessException; @@ -535,25 +655,24 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingRequest * @see #setRepeatingBurst * @see #configureOutputs + * @deprecated Use {@link CameraCaptureSession} instead */ public void flush() throws CameraAccessException; /** - * Close the connection to this camera device. + * Close the connection to this camera device as quickly as possible. * - * <p>After this call, all calls to - * the camera device interface will throw a {@link IllegalStateException}, - * except for calls to close(). Once the device has fully shut down, the - * {@link StateListener#onClosed} callback will be called, and the camera is - * free to be re-opened.</p> + * <p>Immediately after this call, all calls to the camera device or active session interface + * will throw a {@link IllegalStateException}, except for calls to close(). Once the device has + * fully shut down, the {@link StateListener#onClosed} callback will be called, and the camera + * is free to be re-opened.</p> * - * <p>After this call, besides the final {@link StateListener#onClosed} call, no calls to the - * device's {@link StateListener} will occur, and any remaining submitted capture requests will - * not fire their {@link CaptureListener} callbacks.</p> + * <p>Immediately after this call, besides the final {@link StateListener#onClosed} calls, no + * further callbacks from the device or the active session will occur, and any remaining + * submitted capture requests will be discarded, as if + * {@link CameraCaptureSession#abortCaptures} had been called, except that no success or failure + * callbacks will be invoked.</p> * - * <p>To shut down as fast as possible, call the {@link #flush} method and then {@link #close} - * once the flush completes. This will discard some capture requests, but results in faster - * shutdown.</p> */ @Override public void close(); @@ -570,6 +689,7 @@ public interface CameraDevice extends AutoCloseable { * @see #captureBurst * @see #setRepeatingRequest * @see #setRepeatingBurst + * @deprecated Use {@link CameraCaptureSession} instead */ public static abstract class CaptureListener { @@ -646,14 +766,62 @@ public interface CameraDevice extends AutoCloseable { } /** - * This method is called when an image capture has completed and the + * This method is called when an image capture makes partial forward progress; some + * (but not all) results from an image capture are available. + * + * <p>The result provided here will contain some subset of the fields of + * a full result. Multiple {@link #onCaptureProgressed} calls may happen per + * capture; a given result field will only be present in one partial + * capture at most. The final {@link #onCaptureCompleted} call will always + * contain all the fields (in particular, the union of all the fields of all + * the partial results composing the total result).</p> + * + * <p>For each request, some result data might be available earlier than others. The typical + * delay between each partial result (per request) is a single frame interval. + * For performance-oriented use-cases, applications should query the metadata they need + * to make forward progress from the partial results and avoid waiting for the completed + * result.</p> + * + * <p>Each request will generate at least {@code 1} partial results, and at most + * {@link CameraCharacteristics#REQUEST_PARTIAL_RESULT_COUNT} partial results.</p> + * + * <p>Depending on the request settings, the number of partial results per request + * will vary, although typically the partial count could be the same as long as the + * camera device subsystems enabled stay the same.</p> + * + * <p>The default implementation of this method does nothing.</p> + * + * @param camera The CameraDevice sending the callback. + * @param request The request that was given to the CameraDevice + * @param partialResult The partial output metadata from the capture, which + * includes a subset of the {@link TotalCaptureResult} fields. + * + * @see #capture + * @see #captureBurst + * @see #setRepeatingRequest + * @see #setRepeatingBurst + */ + public void onCaptureProgressed(CameraDevice camera, + CaptureRequest request, CaptureResult partialResult) { + // default empty implementation + } + + /** + * This method is called when an image capture has fully completed and all the * result metadata is available. * + * <p>This callback will always fire after the last {@link #onCaptureProgressed}; + * in other words, no more partial results will be delivered once the completed result + * is available.</p> + * + * <p>For performance-intensive use-cases where latency is a factor, consider + * using {@link #onCaptureProgressed} instead.</p> + * * <p>The default implementation of this method does nothing.</p> * * @param camera The CameraDevice sending the callback. * @param request The request that was given to the CameraDevice - * @param result The output metadata from the capture, including the + * @param result The total output metadata from the capture, including the * final capture parameters and the state of the camera system during * capture. * @@ -663,7 +831,7 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingBurst */ public void onCaptureCompleted(CameraDevice camera, - CaptureRequest request, CaptureResult result) { + CaptureRequest request, TotalCaptureResult result) { // default empty implementation } @@ -676,6 +844,10 @@ public interface CameraDevice extends AutoCloseable { * the capture may have been pushed to their respective output * streams.</p> * + * <p>Some partial results may have been delivered before the capture fails; + * however after this callback fires, no more partial results will be delivered by + * {@link #onCaptureProgressed}.</p> + * * <p>The default implementation of this method does nothing.</p> * * @param camera @@ -701,24 +873,57 @@ public interface CameraDevice extends AutoCloseable { * when a capture sequence finishes and all {@link CaptureResult} * or {@link CaptureFailure} for it have been returned via this listener. * + * <p>In total, there will be at least one result/failure returned by this listener + * before this callback is invoked. If the capture sequence is aborted before any + * requests have been processed, {@link #onCaptureSequenceAborted} is invoked instead.</p> + * + * <p>The default implementation does nothing.</p> + * * @param camera * The CameraDevice sending the callback. * @param sequenceId * A sequence ID returned by the {@link #capture} family of functions. - * @param lastFrameNumber + * @param frameNumber * 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() * @see CaptureResult#getSequenceId() * @see CaptureFailure#getSequenceId() + * @see #onCaptureSequenceAborted */ public void onCaptureSequenceCompleted(CameraDevice camera, - int sequenceId, int lastFrameNumber) { + int sequenceId, long frameNumber) { + // default empty implementation + } + + /** + * This method is called independently of the others in CaptureListener, + * when a capture sequence aborts before any {@link CaptureResult} + * or {@link CaptureFailure} for it have been returned via this listener. + * + * <p>Due to the asynchronous nature of the camera device, not all submitted captures + * are immediately processed. It is possible to clear out the pending requests + * by a variety of operations such as {@link CameraDevice#stopRepeating} or + * {@link CameraDevice#flush}. When such an event happens, + * {@link #onCaptureSequenceCompleted} will not be called.</p> + * + * <p>The default implementation does nothing.</p> + * + * @param camera + * The CameraDevice sending the callback. + * @param sequenceId + * A sequence ID returned by the {@link #capture} family of functions. + * + * @see CaptureResult#getFrameNumber() + * @see CaptureFailure#getFrameNumber() + * @see CaptureResult#getSequenceId() + * @see CaptureFailure#getSequenceId() + * @see #onCaptureSequenceCompleted + */ + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { // default empty implementation } } @@ -835,6 +1040,7 @@ public interface CameraDevice extends AutoCloseable { * <p>The default implementation of this method does nothing.</p> * * @param camera the camera device has that become unconfigured + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ public void onUnconfigured(CameraDevice camera) { // Default empty implementation @@ -864,6 +1070,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#captureBurst * @see CameraDevice#setRepeatingBurst * @see CameraDevice#setRepeatingRequest + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ public void onActive(CameraDevice camera) { // Default empty implementation @@ -897,6 +1104,7 @@ public interface CameraDevice extends AutoCloseable { * * @see CameraDevice#configureOutputs * @see CameraDevice#flush + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ public void onBusy(CameraDevice camera) { // Default empty implementation @@ -944,6 +1152,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#configureOutputs * @see CameraDevice#stopRepeating * @see CameraDevice#flush + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ public void onIdle(CameraDevice camera) { // Default empty implementation diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 0fcd598..4a89fe7 100644 --- a/core/java/android/hardware/camera2/CameraManager.java +++ b/core/java/android/hardware/camera2/CameraManager.java @@ -20,6 +20,7 @@ import android.content.Context; import android.hardware.ICameraService; import android.hardware.ICameraServiceListener; import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.legacy.CameraDeviceUserShim; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; import android.hardware.camera2.utils.BinderHolder; @@ -83,9 +84,8 @@ public final class CameraManager { try { CameraBinderDecorator.throwOnError( CameraMetadataNative.nativeSetupGlobalVendorTagDescriptor()); - } catch(CameraRuntimeException e) { - throw new IllegalStateException("Failed to setup camera vendor tags", - e.asChecked()); + } catch (CameraRuntimeException e) { + handleRecoverableSetupErrors(e, "Failed to set up vendor tags"); } try { @@ -194,16 +194,11 @@ public final class CameraManager { // impossible return null; } - return new CameraCharacteristics(info); } /** - * Open a connection to a camera with the given ID. Use - * {@link #getCameraIdList} to get the list of available camera - * devices. Note that even if an id is listed, open may fail if the device - * is disconnected between the calls to {@link #getCameraIdList} and - * {@link #openCamera}. + * Helper for openning a connection to a camera with the given ID. * * @param cameraId The unique identifier of the camera device to open * @param listener The listener for the camera. Must not be null. @@ -216,35 +211,53 @@ public final class CameraManager { * @throws SecurityException if the application does not have permission to * access the camera * @throws IllegalArgumentException if listener or handler is null. + * @return A handle to the newly-created camera device. * * @see #getCameraIdList * @see android.app.admin.DevicePolicyManager#setCameraDisabled */ - private void openCameraDeviceUserAsync(String cameraId, + private CameraDevice openCameraDeviceUserAsync(String cameraId, CameraDevice.StateListener listener, Handler handler) throws CameraAccessException { + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + CameraDevice device = null; try { synchronized (mLock) { ICameraDeviceUser cameraUser; - android.hardware.camera2.impl.CameraDevice device = + android.hardware.camera2.impl.CameraDevice deviceImpl = new android.hardware.camera2.impl.CameraDevice( cameraId, listener, - handler); + handler, + characteristics); BinderHolder holder = new BinderHolder(); - mCameraService.connectDevice(device.getCallbacks(), - Integer.parseInt(cameraId), - mContext.getPackageName(), USE_CALLING_UID, holder); - cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder()); + + ICameraDeviceCallbacks callbacks = deviceImpl.getCallbacks(); + int id = Integer.parseInt(cameraId); + try { + mCameraService.connectDevice(callbacks, id, mContext.getPackageName(), + USE_CALLING_UID, holder); + cameraUser = ICameraDeviceUser.Stub.asInterface(holder.getBinder()); + } catch (CameraRuntimeException e) { + if (e.getReason() == CameraAccessException.CAMERA_DEPRECATED_HAL) { + // Use legacy camera implementation for HAL1 devices + Log.i(TAG, "Using legacy camera HAL."); + cameraUser = CameraDeviceUserShim.connectBinderShim(callbacks, id); + } else { + // Rethrow otherwise + throw e; + } + } // TODO: factor out listener to be non-nested, then move setter to constructor // For now, calling setRemoteDevice will fire initial // onOpened/onUnconfigured callbacks. - device.setRemoteDevice(cameraUser); + deviceImpl.setRemoteDevice(cameraUser); + device = deviceImpl; } } catch (NumberFormatException e) { @@ -255,6 +268,7 @@ public final class CameraManager { } catch (RemoteException e) { // impossible } + return device; } /** @@ -265,20 +279,26 @@ public final class CameraManager { * is disconnected between the calls to {@link #getCameraIdList} and * {@link #openCamera}.</p> * - * <p>If the camera successfully opens after this function call returns, - * {@link CameraDevice.StateListener#onOpened} will be invoked with the - * newly opened {@link CameraDevice} in the unconfigured state.</p> + * <p>Once the camera is successfully opened, {@link CameraDevice.StateListener#onOpened} will + * be invoked with the newly opened {@link CameraDevice}. The camera device can then be set up + * for operation by calling {@link CameraDevice#createCaptureSession} and + * {@link CameraDevice#createCaptureRequest}</p> * + * <!-- + * <p>Since the camera device will be opened asynchronously, any asynchronous operations done + * on the returned CameraDevice instance will be queued up until the device startup has + * completed and the listener's {@link CameraDevice.StateListener#onOpened onOpened} method is + * called. The pending operations are then processed in order.</p> + * --> * <p>If the camera becomes disconnected during initialization * after this function call returns, * {@link CameraDevice.StateListener#onDisconnected} with a * {@link CameraDevice} in the disconnected state (and * {@link CameraDevice.StateListener#onOpened} will be skipped).</p> * - * <p>If the camera fails to initialize after this function call returns, - * {@link CameraDevice.StateListener#onError} will be invoked with a - * {@link CameraDevice} in the error state (and - * {@link CameraDevice.StateListener#onOpened} will be skipped).</p> + * <p>If opening the camera device fails, then the device listener's + * {@link CameraDevice.StateListener#onError onError} method will be called, and subsequent + * calls on the camera device will throw an {@link IllegalStateException}.</p> * * @param cameraId * The unique identifier of the camera device to open @@ -405,6 +425,18 @@ public final class CameraManager { return mDeviceIdList; } + private void handleRecoverableSetupErrors(CameraRuntimeException e, String msg) { + int problem = e.getReason(); + switch (problem) { + case CameraAccessException.CAMERA_DISCONNECTED: + String errorMsg = CameraAccessException.getDefaultMessage(problem); + Log.w(TAG, msg + ": " + errorMsg); + break; + default: + throw new IllegalStateException(msg, e.asChecked()); + } + } + // TODO: this class needs unit tests // TODO: extract class into top level private class CameraServiceListener extends ICameraServiceListener.Stub { diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index 6e38a22..b3e165e 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -16,7 +16,7 @@ package android.hardware.camera2; -import android.hardware.camera2.impl.CameraMetadataNative; +import android.util.Log; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -35,7 +35,7 @@ import java.util.List; * * <p> * All instances of CameraMetadata are immutable. The list of keys with {@link #getKeys()} - * never changes, nor do the values returned by any key with {@link #get} throughout + * never changes, nor do the values returned by any key with {@code #get} throughout * the lifetime of the object. * </p> * @@ -43,7 +43,10 @@ import java.util.List; * @see CameraManager * @see CameraCharacteristics **/ -public abstract class CameraMetadata { +public abstract class CameraMetadata<TKey> { + + private static final String TAG = "CameraMetadataAb"; + private static final boolean VERBOSE = false; /** * Set a camera metadata field to a value. The field definitions can be @@ -73,8 +76,15 @@ public abstract class CameraMetadata { * * @param key The metadata field to read. * @return The value of that key, or {@code null} if the field is not set. + * + * @hide */ - public abstract <T> T get(Key<T> key); + protected abstract <T> T getProtected(TKey key); + + /** + * @hide + */ + protected abstract Class<TKey> getKeyClass(); /** * Returns a list of the keys contained in this map. @@ -82,14 +92,16 @@ public abstract class CameraMetadata { * <p>The list returned is not modifiable, so any attempts to modify it will throw * a {@code UnsupportedOperationException}.</p> * - * <p>All values retrieved by a key from this list with {@link #get} are guaranteed to be + * <p>All values retrieved by a key from this list with {@code #get} are guaranteed to be * non-{@code null}. Each key is only listed once in the list. The order of the keys * is undefined.</p> * * @return List of the keys contained in this map. */ - public List<Key<?>> getKeys() { - return Collections.unmodifiableList(getKeysStatic(this.getClass(), this)); + @SuppressWarnings("unchecked") + public List<TKey> getKeys() { + Class<CameraMetadata<TKey>> thisClass = (Class<CameraMetadata<TKey>>) getClass(); + return Collections.unmodifiableList(getKeysStatic(thisClass, getKeyClass(), this)); } /** @@ -100,24 +112,31 @@ public abstract class CameraMetadata { * Optionally, if {@code instance} is not null, then filter out any keys with null values. * </p> */ - /*package*/ static ArrayList<Key<?>> getKeysStatic(Class<? extends CameraMetadata> type, - CameraMetadata instance) { - ArrayList<Key<?>> keyList = new ArrayList<Key<?>>(); + /*package*/ @SuppressWarnings("unchecked") + static <TKey> ArrayList<TKey> getKeysStatic( + Class<?> type, Class<TKey> keyClass, + CameraMetadata<TKey> instance) { + + if (VERBOSE) Log.v(TAG, "getKeysStatic for " + type); + + ArrayList<TKey> keyList = new ArrayList<TKey>(); Field[] fields = type.getDeclaredFields(); for (Field field : fields) { // Filter for Keys that are public - if (field.getType().isAssignableFrom(Key.class) && + if (field.getType().isAssignableFrom(keyClass) && (field.getModifiers() & Modifier.PUBLIC) != 0) { - Key<?> key; + + TKey key; try { - key = (Key<?>) field.get(instance); + key = (TKey) field.get(instance); } catch (IllegalAccessException e) { throw new AssertionError("Can't get IllegalAccessException", e); } catch (IllegalArgumentException e) { throw new AssertionError("Can't get IllegalArgumentException", e); } - if (instance == null || instance.get(key) != null) { + + if (instance == null || instance.getProtected(key) != null) { keyList.add(key); } } @@ -126,79 +145,6 @@ public abstract class CameraMetadata { return keyList; } - public static class Key<T> { - - private boolean mHasTag; - private int mTag; - private final Class<T> mType; - private final String mName; - - /** - * @hide - */ - public Key(String name, Class<T> type) { - if (name == null) { - throw new NullPointerException("Key needs a valid name"); - } else if (type == null) { - throw new NullPointerException("Type needs to be non-null"); - } - mName = name; - mType = type; - } - - public final String getName() { - return mName; - } - - @Override - public final int hashCode() { - return mName.hashCode(); - } - - @Override - @SuppressWarnings("unchecked") - public final boolean equals(Object o) { - if (this == o) { - return true; - } - - if (!(o instanceof Key)) { - return false; - } - - Key lhs = (Key) o; - - return mName.equals(lhs.mName) && mType.equals(lhs.mType); - } - - /** - * <p> - * Get the tag corresponding to this key. This enables insertion into the - * native metadata. - * </p> - * - * <p>This value is looked up the first time, and cached subsequently.</p> - * - * @return The tag numeric value corresponding to the string - * - * @hide - */ - public final int getTag() { - if (!mHasTag) { - mTag = CameraMetadataNative.getTag(mName); - mHasTag = true; - } - return mTag; - } - - /** - * @hide - */ - public final Class<T> getType() { - return mType; - } - } - /*@O~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * The enum values below this point are generated from metadata * definitions in /system/media/camera/docs. Do not modify by hand or @@ -290,9 +236,17 @@ public abstract class CameraMetadata { /** * <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> + * as auto exposure, and auto focus can be bypassed). + * The camera device supports basic manual control of the sensor image + * acquisition related stages. This means the following controls are + * guaranteed to be supported:</p> * <ul> + * <li>Manual frame duration control<ul> + * <li>{@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration}</li> + * <li>{@link CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION android.sensor.info.maxFrameDuration}</li> + * <li>{@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap}</li> + * </ul> + * </li> * <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> @@ -301,7 +255,6 @@ public abstract class CameraMetadata { * <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> @@ -320,11 +273,15 @@ public abstract class CameraMetadata { * <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> + * <p>A given camera device may also support additional manual sensor controls, + * but this capability only covers the above list of controls.</p> * * @see CaptureRequest#BLACK_LEVEL_LOCK - * @see CameraCharacteristics#SENSOR_BASE_GAIN_FACTOR + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP * @see CaptureRequest#SENSOR_EXPOSURE_TIME + * @see CaptureRequest#SENSOR_FRAME_DURATION * @see CameraCharacteristics#SENSOR_INFO_EXPOSURE_TIME_RANGE + * @see CameraCharacteristics#SENSOR_INFO_MAX_FRAME_DURATION * @see CameraCharacteristics#SENSOR_INFO_SENSITIVITY_RANGE * @see CaptureRequest#SENSOR_SENSITIVITY * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES @@ -332,12 +289,12 @@ public abstract class CameraMetadata { public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR = 2; /** - * <p>TODO: This should be @hide</p> + * <p>The camera device post-processing stages can be manually controlled. + * The camera device supports basic manual control of the image post-processing + * stages. This means the following controls are guaranteed to be supported:</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_CURVE android.tonemap.curve}</li> * <li>{@link CaptureRequest#TONEMAP_MODE android.tonemap.mode}</li> * <li>{@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS android.tonemap.maxCurvePoints}</li> * </ul> @@ -348,8 +305,8 @@ public abstract class CameraMetadata { * </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> + * <li>android.statistics.lensShadingMap</li> + * <li>android.lens.info.shadingMapSize</li> * </ul> * </li> * </ul> @@ -357,19 +314,17 @@ public abstract class CameraMetadata { * 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> + * <p>A given camera device may also support additional post-processing + * controls, but this capability only covers the above list of controls.</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 CaptureRequest#TONEMAP_CURVE * @see CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS * @see CaptureRequest#TONEMAP_MODE * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES */ - public static final int REQUEST_AVAILABLE_CAPABILITIES_GCAM = 3; + public static final int REQUEST_AVAILABLE_CAPABILITIES_MANUAL_POST_PROCESSING = 3; /** * <p>The camera device supports the Zero Shutter Lag use case.</p> @@ -411,18 +366,20 @@ public abstract class CameraMetadata { public static final int REQUEST_AVAILABLE_CAPABILITIES_DNG = 5; // - // Enumeration values for CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + // Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE // /** - * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + * <p>The camera device will only support centered crop regions.</p> + * @see CameraCharacteristics#SCALER_CROPPING_TYPE */ - public static final int SCALER_AVAILABLE_STREAM_CONFIGURATIONS_OUTPUT = 0; + public static final int SCALER_CROPPING_TYPE_CENTER_ONLY = 0; /** - * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS + * <p>The camera device will support arbitrarily chosen crop regions.</p> + * @see CameraCharacteristics#SCALER_CROPPING_TYPE */ - public static final int SCALER_AVAILABLE_STREAM_CONFIGURATIONS_INPUT = 1; + public static final int SCALER_CROPPING_TYPE_FREEFORM = 1; // // Enumeration values for CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT @@ -1344,8 +1301,7 @@ public abstract class CameraMetadata { /** * <p>If the flash is available and charged, fire flash - * for this capture based on android.flash.firingPower and - * android.flash.firingTime.</p> + * for this capture.</p> * @see CaptureRequest#FLASH_MODE */ public static final int FLASH_MODE_SINGLE = 1; @@ -1603,17 +1559,14 @@ public abstract class CameraMetadata { /** * <p>Use the tone mapping curve specified in - * the android.tonemap.curve* entries.</p> + * the {@link CaptureRequest#TONEMAP_CURVE 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> + * {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}.</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_CURVE * @see CaptureRequest#TONEMAP_MODE */ public static final int TONEMAP_MODE_CONTRAST_CURVE = 0; diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index f161f3a..d4dfdd5 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -16,12 +16,18 @@ package android.hardware.camera2; +import android.hardware.camera2.CameraCharacteristics.Key; import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.TypeReference; import android.os.Parcel; import android.os.Parcelable; +import android.util.Rational; import android.view.Surface; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; @@ -55,7 +61,98 @@ import java.util.Objects; * @see CameraDevice#setRepeatingRequest * @see CameraDevice#createCaptureRequest */ -public final class CaptureRequest extends CameraMetadata implements Parcelable { +public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> + implements Parcelable { + + /** + * A {@code Key} is used to do capture request field lookups with + * {@link CaptureResult#get} or to set fields with + * {@link CaptureRequest.Builder#set(Key, Object)}. + * + * <p>For example, to set the crop rectangle for the next capture: + * <code><pre> + * Rect cropRectangle = new Rect(0, 0, 640, 480); + * captureRequestBuilder.set(SCALER_CROP_REGION, cropRectangle); + * </pre></code> + * </p> + * + * <p>To enumerate over all possible keys for {@link CaptureResult}, see + * {@link CameraCharacteristics#getAvailableCaptureResultKeys}.</p> + * + * @see CaptureResult#get + * @see CameraCharacteristics#getAvailableCaptureResultKeys + */ + public final static class Key<T> { + private final CameraMetadataNative.Key<T> mKey; + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, Class<T> type) { + mKey = new CameraMetadataNative.Key<T>(name, type); + } + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, TypeReference<T> typeReference) { + mKey = new CameraMetadataNative.Key<T>(name, typeReference); + } + + /** + * Return a camelCase, period separated name formatted like: + * {@code "root.section[.subsections].name"}. + * + * <p>Built-in keys exposed by the Android SDK are always prefixed with {@code "android."}; + * keys that are device/platform-specific are prefixed with {@code "com."}.</p> + * + * <p>For example, {@code CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP} would + * have a name of {@code "android.scaler.streamConfigurationMap"}; whereas a device + * specific key might look like {@code "com.google.nexus.data.private"}.</p> + * + * @return String representation of the key name + */ + public String getName() { + return mKey.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public final int hashCode() { + return mKey.hashCode(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public final boolean equals(Object o) { + return o instanceof Key && ((Key<T>)o).mKey.equals(mKey); + } + + /** + * Visible for CameraMetadataNative implementation only; do not use. + * + * TODO: Make this private or remove it altogether. + * + * @hide + */ + public CameraMetadataNative.Key<T> getNativeKey() { + return mKey; + } + + @SuppressWarnings({ "unchecked" }) + /*package*/ Key(CameraMetadataNative.Key<?> nativeKey) { + mKey = (CameraMetadataNative.Key<T>) nativeKey; + } + } private final HashSet<Surface> mSurfaceSet; private final CameraMetadataNative mSettings; @@ -90,17 +187,58 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * Used by the Builder to create a mutable CaptureRequest. */ private CaptureRequest(CameraMetadataNative settings) { - mSettings = settings; + mSettings = CameraMetadataNative.move(settings); mSurfaceSet = new HashSet<Surface>(); } - @SuppressWarnings("unchecked") - @Override + /** + * Get a capture request field value. + * + * <p>The field definitions can be found in {@link CaptureRequest}.</p> + * + * <p>Querying the value for the same key more than once will return a value + * which is equal to the previous queried value.</p> + * + * @throws IllegalArgumentException if the key was not valid + * + * @param key The result field to read. + * @return The value of that key, or {@code null} if the field is not set. + */ public <T> T get(Key<T> key) { return mSettings.get(key); } /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected <T> T getProtected(Key<?> key) { + return (T) mSettings.get(key); + } + + /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected Class<Key<?>> getKeyClass() { + Object thisClass = Key.class; + return (Class<Key<?>>)thisClass; + } + + /** + * {@inheritDoc} + */ + @Override + public List<Key<?>> getKeys() { + // Force the javadoc for this function to show up on the CaptureRequest page + return super.getKeys(); + } + + /** * Retrieve the tag for this request, if any. * * <p>This tag is not used for anything by the camera device, but can be @@ -169,7 +307,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * @param in The parcel from which the object should be read * @hide */ - public void readFromParcel(Parcel in) { + private void readFromParcel(Parcel in) { mSettings.readFromParcel(in); mSurfaceSet.clear(); @@ -198,6 +336,20 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { } /** + * @hide + */ + public boolean containsTarget(Surface surface) { + return mSurfaceSet.contains(surface); + } + + /** + * @hide + */ + public Collection<Surface> getTargets() { + return Collections.unmodifiableCollection(mSurfaceSet); + } + + /** * A builder for capture requests. * * <p>To obtain a builder instance, use the @@ -385,30 +537,24 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * * @see CaptureRequest#COLOR_CORRECTION_MODE */ - public static final Key<Rational[]> COLOR_CORRECTION_TRANSFORM = - new Key<Rational[]>("android.colorCorrection.transform", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> COLOR_CORRECTION_TRANSFORM = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.colorCorrection.transform", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <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 <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 - * {@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> + * <p>These per-channel gains are 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 {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is + * TRANSFORM_MATRIX.</p> + * <p>The gains in the result metadata are 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); + public static final Key<android.hardware.camera2.params.RggbChannelVector> COLOR_CORRECTION_GAINS = + new Key<android.hardware.camera2.params.RggbChannelVector>("android.colorCorrection.gains", android.hardware.camera2.params.RggbChannelVector.class); /** * <p>The desired setting for the camera device's auto-exposure @@ -458,7 +604,19 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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 EV. Note that this control will only be effective + * if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} <code>!=</code> OFF. This control will take effect even when + * {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} <code>== true</code>.</p> + * <p>In the event of exposure compensation value being changed, camera device + * may take several frames to reach the newly requested exposure target. + * During that time, {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} field will be in the SEARCHING + * state. Once the new exposure target is reached, {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} will + * change from SEARCHING to either CONVERGED, LOCKED (if AE lock is enabled), or + * FLASH_REQUIRED (if the scene is too dark for still capture).</p> + * + * @see CaptureRequest#CONTROL_AE_LOCK + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureResult#CONTROL_AE_STATE */ public static final Key<Integer> CONTROL_AE_EXPOSURE_COMPENSATION = new Key<Integer>("android.control.aeExposureCompensation", int.class); @@ -469,6 +627,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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>When {@link CaptureRequest#CONTROL_AE_EXPOSURE_COMPENSATION android.control.aeExposureCompensation} is changed, even if the AE lock + * is ON, the camera device will still adjust its exposure value.</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}) @@ -477,6 +637,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * {@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_EXPOSURE_COMPENSATION * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER * @see CaptureResult#CONTROL_AE_STATE @@ -526,26 +687,27 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { /** * <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 * ({@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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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> + * outside the used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.aeRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Range over which fps can be adjusted to @@ -555,8 +717,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * * @see CaptureRequest#SENSOR_EXPOSURE_TIME */ - public static final Key<int[]> CONTROL_AE_TARGET_FPS_RANGE = - new Key<int[]>("android.control.aeTargetFpsRange", int[].class); + public static final Key<android.util.Range<Integer>> CONTROL_AE_TARGET_FPS_RANGE = + new Key<android.util.Range<Integer>>("android.control.aeTargetFpsRange", new TypeReference<android.util.Range<Integer>>() {{ }}); /** * <p>Whether the camera device will trigger a precapture @@ -601,26 +763,27 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { /** * <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 * ({@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 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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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 used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AF_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.afRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Whether the camera device will trigger autofocus for this request.</p> @@ -687,27 +850,27 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { /** * <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 * ({@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 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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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 used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.awbRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Information to the camera device 3A (auto-exposure, @@ -901,8 +1064,15 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { new Key<Integer>("android.hotPixel.mode", int.class); /** + * <p>A location object to use when generating image GPS metadata.</p> + */ + public static final Key<android.location.Location> JPEG_GPS_LOCATION = + new Key<android.location.Location>("android.jpeg.gpsLocation", android.location.Location.class); + + /** * <p>GPS coordinates to include in output JPEG * EXIF</p> + * @hide */ public static final Key<double[]> JPEG_GPS_COORDINATES = new Key<double[]>("android.jpeg.gpsCoordinates", double[].class); @@ -910,6 +1080,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { /** * <p>32 characters describing GPS algorithm to * include in EXIF</p> + * @hide */ public static final Key<String> JPEG_GPS_PROCESSING_METHOD = new Key<String>("android.jpeg.gpsProcessingMethod", String.class); @@ -917,6 +1088,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { /** * <p>Time GPS fix was made to include in * EXIF</p> + * @hide */ public static final Key<Long> JPEG_GPS_TIMESTAMP = new Key<Long>("android.jpeg.gpsTimestamp", long.class); @@ -949,9 +1121,15 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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> + * <p>If the thumbnail image aspect ratio differs from the JPEG primary image aspect + * ratio, the camera device creates the thumbnail by cropping it from the primary image. + * For example, if the primary image has 4:3 aspect ratio, the thumbnail image has + * 16:9 aspect ratio, the primary image will be cropped vertically (letterbox) to + * generate the thumbnail image. The thumbnail image will always have a smaller Field + * Of View (FOV) than the primary image when aspect ratios differ.</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); + public static final Key<android.util.Size> JPEG_THUMBNAIL_SIZE = + new Key<android.util.Size>("android.jpeg.thumbnailSize", android.util.Size.class); /** * <p>The ratio of lens focal length to the effective @@ -1094,8 +1272,11 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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>The crop region is applied after the RAW to other color space (e.g. YUV) + * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, + * it is not croppable. The crop region will be ignored by raw streams.</p> + * <p>For non-raw streams, any additional per-stream cropping will + * 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 @@ -1170,7 +1351,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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. + * is provided via the {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} field + * using StreamConfigurationMap#getOutputMinFrameDuration(int, Size). * 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 @@ -1180,7 +1362,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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 + * looking it up in {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} using + * StreamConfigurationMap#getOutputMinFrameDuration(int, Size) (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 @@ -1188,7 +1371,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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 + * StreamConfigurationMap#getOutputStallDuration(int,Size) using its + * respective size/format), 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> @@ -1199,10 +1383,9 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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> + * StreamConfigurationMap#getOutputStallDuration(int,Size).</p> * - * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP */ public static final Key<Long> SENSOR_FRAME_DURATION = new Key<Long>("android.sensor.frameDuration", long.class); @@ -1260,8 +1443,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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 + * shading map with size specified as <code>android.lens.info.shadingMapSize = [ 4, 3 ]</code>, + * the output 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, @@ -1273,11 +1456,17 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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> + * lens shading map data in android.statistics.lensShadingMap, with size specified + * by android.lens.info.shadingMapSize; the returned shading map data will be the one + * applied by the camera device for this capture request.</p> + * <p>The shading map data may depend on the AE and AWB statistics, therefore the reliability + * of the map data may be affected by the AE and AWB algorithms. When AE and AWB are in + * AUTO modes({@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} <code>!=</code> OFF and {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} <code>!=</code> OFF), + * to get best results, it is recommended that the applications wait for the AE and AWB to + * be converged before using the returned shading map data.</p> * - * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE - * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AWB_MODE * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE * @see #SHADING_MODE_OFF * @see #SHADING_MODE_FAST @@ -1318,10 +1507,8 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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 + * 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 */ @@ -1332,10 +1519,10 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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> + * <p>See android.tonemap.curveRed for more details.</p> * - * @see CaptureRequest#TONEMAP_CURVE_RED * @see CaptureRequest#TONEMAP_MODE + * @hide */ public static final Key<float[]> TONEMAP_CURVE_BLUE = new Key<float[]>("android.tonemap.curveBlue", float[].class); @@ -1344,10 +1531,10 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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> + * <p>See android.tonemap.curveRed for more details.</p> * - * @see CaptureRequest#TONEMAP_CURVE_RED * @see CaptureRequest#TONEMAP_MODE + * @hide */ public static final Key<float[]> TONEMAP_CURVE_GREEN = new Key<float[]>("android.tonemap.curveGreen", float[].class); @@ -1357,7 +1544,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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} = + * <pre><code>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 @@ -1373,15 +1560,15 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * 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 ] + * <pre><code>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 ] + * <pre><code>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} = [ + * <pre><code>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, @@ -1389,7 +1576,7 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * </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} = [ + * <pre><code>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, @@ -1397,14 +1584,67 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * </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 + * @hide */ public static final Key<float[]> TONEMAP_CURVE_RED = new Key<float[]>("android.tonemap.curveRed", float[].class); /** + * <p>Tonemapping / contrast / gamma curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} + * is CONTRAST_CURVE.</p> + * <p>The tonemapCurve consist of three curves for each of red, green, and blue + * channels respectively. The following example uses the red channel as an + * example. The same logic applies to green and blue channel. + * Each channel's curve is defined by an array of control points:</p> + * <pre><code>curveRed = + * [ P0(in, out), P1(in, out), P2(in, out), P3(in, out), ..., PN(in, out) ] + * 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>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>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>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>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 CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS + * @see CaptureRequest#TONEMAP_MODE + */ + public static final Key<android.hardware.camera2.params.TonemapCurve> TONEMAP_CURVE = + new Key<android.hardware.camera2.params.TonemapCurve>("android.tonemap.curve", android.hardware.camera2.params.TonemapCurve.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 @@ -1420,18 +1660,15 @@ public final class CaptureRequest extends CameraMetadata implements Parcelable { * <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}. + * emit its own tonemap curve in {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}. * 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 + * <p>If a request is sent with CONTRAST_CURVE 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_CURVE * @see CaptureRequest#TONEMAP_MODE * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index 51ea447..7d07c92 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -17,11 +17,16 @@ package android.hardware.camera2; import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.TypeReference; +import android.util.Log; +import android.util.Rational; + +import java.util.List; /** - * <p>The results of a single image capture from the image sensor.</p> + * <p>The subset of the results of a single image capture from the image sensor.</p> * - * <p>Contains the final configuration for the capture hardware (sensor, lens, + * <p>Contains a subset of the final configuration for the capture hardware (sensor, lens, * flash), the processing pipeline, the control algorithms, and the output * buffers.</p> * @@ -31,8 +36,106 @@ import android.hardware.camera2.impl.CameraMetadataNative; * capture. The result also includes additional metadata about the state of the * camera device during the capture.</p> * + * <p>Not all properties returned by {@link CameraCharacteristics#getAvailableCaptureResultKeys()} + * are necessarily available. Some results are {@link CaptureResult partial} and will + * not have every key set. Only {@link TotalCaptureResult total} results are guaranteed to have + * every key available that was enabled by the request.</p> + * + * <p>{@link CaptureResult} objects are immutable.</p> + * */ -public final class CaptureResult extends CameraMetadata { +public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { + + private static final String TAG = "CaptureResult"; + private static final boolean VERBOSE = false; + + /** + * A {@code Key} is used to do capture result field lookups with + * {@link CaptureResult#get}. + * + * <p>For example, to get the timestamp corresponding to the exposure of the first row: + * <code><pre> + * long timestamp = captureResult.get(CaptureResult.SENSOR_TIMESTAMP); + * </pre></code> + * </p> + * + * <p>To enumerate over all possible keys for {@link CaptureResult}, see + * {@link CameraCharacteristics#getAvailableCaptureResultKeys}.</p> + * + * @see CaptureResult#get + * @see CameraCharacteristics#getAvailableCaptureResultKeys + */ + public final static class Key<T> { + private final CameraMetadataNative.Key<T> mKey; + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, Class<T> type) { + mKey = new CameraMetadataNative.Key<T>(name, type); + } + + /** + * Visible for testing and vendor extensions only. + * + * @hide + */ + public Key(String name, TypeReference<T> typeReference) { + mKey = new CameraMetadataNative.Key<T>(name, typeReference); + } + + /** + * Return a camelCase, period separated name formatted like: + * {@code "root.section[.subsections].name"}. + * + * <p>Built-in keys exposed by the Android SDK are always prefixed with {@code "android."}; + * keys that are device/platform-specific are prefixed with {@code "com."}.</p> + * + * <p>For example, {@code CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP} would + * have a name of {@code "android.scaler.streamConfigurationMap"}; whereas a device + * specific key might look like {@code "com.google.nexus.data.private"}.</p> + * + * @return String representation of the key name + */ + public String getName() { + return mKey.getName(); + } + + /** + * {@inheritDoc} + */ + @Override + public final int hashCode() { + return mKey.hashCode(); + } + + /** + * {@inheritDoc} + */ + @SuppressWarnings("unchecked") + @Override + public final boolean equals(Object o) { + return o instanceof Key && ((Key<T>)o).mKey.equals(mKey); + } + + /** + * Visible for CameraMetadataNative implementation only; do not use. + * + * TODO: Make this private or remove it altogether. + * + * @hide + */ + public CameraMetadataNative.Key<T> getNativeKey() { + return mKey; + } + + @SuppressWarnings({ "unchecked" }) + /*package*/ Key(CameraMetadataNative.Key<?> nativeKey) { + mKey = (CameraMetadataNative.Key<T>) nativeKey; + } + } private final CameraMetadataNative mResults; private final CaptureRequest mRequest; @@ -51,31 +154,119 @@ public final class CaptureResult extends CameraMetadata { throw new IllegalArgumentException("parent was null"); } - mResults = results; + mResults = CameraMetadataNative.move(results); + if (mResults.isEmpty()) { + throw new AssertionError("Results must not be empty"); + } mRequest = parent; mSequenceId = sequenceId; } - @Override + /** + * Returns a copy of the underlying {@link CameraMetadataNative}. + * @hide + */ + public CameraMetadataNative getNativeCopy() { + return new CameraMetadataNative(mResults); + } + + /** + * Creates a request-less result. + * + * <p><strong>For testing only.</strong></p> + * @hide + */ + public CaptureResult(CameraMetadataNative results, int sequenceId) { + if (results == null) { + throw new IllegalArgumentException("results was null"); + } + + mResults = CameraMetadataNative.move(results); + if (mResults.isEmpty()) { + throw new AssertionError("Results must not be empty"); + } + + mRequest = null; + mSequenceId = sequenceId; + } + + /** + * Get a capture result field value. + * + * <p>The field definitions can be found in {@link CaptureResult}.</p> + * + * <p>Querying the value for the same key more than once will return a value + * which is equal to the previous queried value.</p> + * + * @throws IllegalArgumentException if the key was not valid + * + * @param key The result field to read. + * @return The value of that key, or {@code null} if the field is not set. + */ public <T> T get(Key<T> key) { - return mResults.get(key); + T value = mResults.get(key); + if (VERBOSE) Log.v(TAG, "#get for Key = " + key.getName() + ", returned value = " + value); + return value; + } + + /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected <T> T getProtected(Key<?> key) { + return (T) mResults.get(key); + } + + /** + * {@inheritDoc} + * @hide + */ + @SuppressWarnings("unchecked") + @Override + protected Class<Key<?>> getKeyClass() { + Object thisClass = Key.class; + return (Class<Key<?>>)thisClass; + } + + /** + * Dumps the native metadata contents to logcat. + * + * <p>Visibility for testing/debugging only. The results will not + * include any synthesized keys, as they are invisible to the native layer.</p> + * + * @hide + */ + public void dumpToLog() { + mResults.dumpToLog(); + } + + /** + * {@inheritDoc} + */ + @Override + public List<Key<?>> getKeys() { + // Force the javadoc for this function to show up on the CaptureResult page + return super.getKeys(); } /** * Get the request associated with this result. * - * <p>Whenever a request is successfully captured, with - * {@link CameraDevice.CaptureListener#onCaptureCompleted}, - * the {@code result}'s {@code getRequest()} will return that {@code request}. + * <p>Whenever a request has been fully or partially captured, with + * {@link CameraDevice.CaptureListener#onCaptureCompleted} or + * {@link CameraDevice.CaptureListener#onCaptureProgressed}, the {@code result}'s + * {@code getRequest()} will return that {@code request}. * </p> * - * <p>In particular, + * <p>For example, * <code><pre>cameraDevice.capture(someRequest, new CaptureListener() { * {@literal @}Override * void onCaptureCompleted(CaptureRequest myRequest, CaptureResult myResult) { * assert(myResult.getRequest.equals(myRequest) == true); * } - * }; + * }, null); * </code></pre> * </p> * @@ -98,6 +289,7 @@ public final class CaptureResult extends CameraMetadata { * @return int frame number */ public int getFrameNumber() { + // TODO: @hide REQUEST_FRAME_COUNT return get(REQUEST_FRAME_COUNT); } @@ -111,6 +303,7 @@ public final class CaptureResult extends CameraMetadata { * @return int The ID for the sequence of requests that this capture result is a part of * * @see CameraDevice.CaptureListener#onCaptureSequenceCompleted + * @see CameraDevice.CaptureListener#onCaptureSequenceAborted */ public int getSequenceId() { return mSequenceId; @@ -190,42 +383,24 @@ public final class CaptureResult extends CameraMetadata { * * @see CaptureRequest#COLOR_CORRECTION_MODE */ - public static final Key<Rational[]> COLOR_CORRECTION_TRANSFORM = - new Key<Rational[]>("android.colorCorrection.transform", Rational[].class); + public static final Key<android.hardware.camera2.params.ColorSpaceTransform> COLOR_CORRECTION_TRANSFORM = + new Key<android.hardware.camera2.params.ColorSpaceTransform>("android.colorCorrection.transform", android.hardware.camera2.params.ColorSpaceTransform.class); /** * <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 <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 - * {@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> + * <p>These per-channel gains are 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 {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is + * TRANSFORM_MATRIX.</p> + * <p>The gains in the result metadata are 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 - * CAMERA2_TRIGGER_PRECAPTURE_METERING trigger received yet - * by HAL. Always updated even if AE algorithm ignores the - * trigger</p> - * @hide - */ - public static final Key<Integer> CONTROL_AE_PRECAPTURE_ID = - new Key<Integer>("android.control.aePrecaptureId", int.class); + public static final Key<android.hardware.camera2.params.RggbChannelVector> COLOR_CORRECTION_GAINS = + new Key<android.hardware.camera2.params.RggbChannelVector>("android.colorCorrection.gains", android.hardware.camera2.params.RggbChannelVector.class); /** * <p>The desired setting for the camera device's auto-exposure @@ -275,7 +450,19 @@ public final class CaptureResult extends CameraMetadata { * 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 EV. Note that this control will only be effective + * if {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} <code>!=</code> OFF. This control will take effect even when + * {@link CaptureRequest#CONTROL_AE_LOCK android.control.aeLock} <code>== true</code>.</p> + * <p>In the event of exposure compensation value being changed, camera device + * may take several frames to reach the newly requested exposure target. + * During that time, {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} field will be in the SEARCHING + * state. Once the new exposure target is reached, {@link CaptureResult#CONTROL_AE_STATE android.control.aeState} will + * change from SEARCHING to either CONVERGED, LOCKED (if AE lock is enabled), or + * FLASH_REQUIRED (if the scene is too dark for still capture).</p> + * + * @see CaptureRequest#CONTROL_AE_LOCK + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureResult#CONTROL_AE_STATE */ public static final Key<Integer> CONTROL_AE_EXPOSURE_COMPENSATION = new Key<Integer>("android.control.aeExposureCompensation", int.class); @@ -286,6 +473,8 @@ public final class CaptureResult extends CameraMetadata { * <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>When {@link CaptureRequest#CONTROL_AE_EXPOSURE_COMPENSATION android.control.aeExposureCompensation} is changed, even if the AE lock + * is ON, the camera device will still adjust its exposure value.</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}) @@ -294,6 +483,7 @@ public final class CaptureResult extends CameraMetadata { * {@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_EXPOSURE_COMPENSATION * @see CaptureRequest#CONTROL_AE_MODE * @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER * @see CaptureResult#CONTROL_AE_STATE @@ -343,26 +533,27 @@ public final class CaptureResult extends CameraMetadata { /** * <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 * ({@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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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> + * outside the used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AE_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.aeRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Range over which fps can be adjusted to @@ -372,8 +563,8 @@ public final class CaptureResult extends CameraMetadata { * * @see CaptureRequest#SENSOR_EXPOSURE_TIME */ - public static final Key<int[]> CONTROL_AE_TARGET_FPS_RANGE = - new Key<int[]>("android.control.aeTargetFpsRange", int[].class); + public static final Key<android.util.Range<Integer>> CONTROL_AE_TARGET_FPS_RANGE = + new Key<android.util.Range<Integer>>("android.control.aeTargetFpsRange", new TypeReference<android.util.Range<Integer>>() {{ }}); /** * <p>Whether the camera device will trigger a precapture @@ -616,26 +807,27 @@ public final class CaptureResult extends CameraMetadata { /** * <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 * ({@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 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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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 used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AF_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.afRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Whether the camera device will trigger autofocus for this request.</p> @@ -1053,17 +1245,6 @@ 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 - * received yet by HAL. Always updated even if AF algorithm - * 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 locked to its * latest calculated values.</p> * <p>Note that AWB lock is only meaningful for AUTO @@ -1110,27 +1291,27 @@ public final class CaptureResult extends CameraMetadata { /** * <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 * ({@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 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> + * bottom-right pixel in the active pixel array.</p> + * <p>The weight must range from 0 to 1000, and represents a weight + * for every pixel in the area. This means that a large metering area + * with the same weight as a smaller area will have more effect in + * the metering result. Metering areas can partially overlap and the + * camera device will add the weights in the overlap region.</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 used {@link CaptureRequest#SCALER_CROP_REGION android.scaler.cropRegion} returned in capture result metadata, + * the camera device will ignore the sections outside the region and output the + * used sections in the result 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); + public static final Key<android.hardware.camera2.params.MeteringRectangle[]> CONTROL_AWB_REGIONS = + new Key<android.hardware.camera2.params.MeteringRectangle[]>("android.control.awbRegions", android.hardware.camera2.params.MeteringRectangle[].class); /** * <p>Information to the camera device 3A (auto-exposure, @@ -1471,8 +1652,15 @@ public final class CaptureResult extends CameraMetadata { new Key<Integer>("android.hotPixel.mode", int.class); /** + * <p>A location object to use when generating image GPS metadata.</p> + */ + public static final Key<android.location.Location> JPEG_GPS_LOCATION = + new Key<android.location.Location>("android.jpeg.gpsLocation", android.location.Location.class); + + /** * <p>GPS coordinates to include in output JPEG * EXIF</p> + * @hide */ public static final Key<double[]> JPEG_GPS_COORDINATES = new Key<double[]>("android.jpeg.gpsCoordinates", double[].class); @@ -1480,6 +1668,7 @@ public final class CaptureResult extends CameraMetadata { /** * <p>32 characters describing GPS algorithm to * include in EXIF</p> + * @hide */ public static final Key<String> JPEG_GPS_PROCESSING_METHOD = new Key<String>("android.jpeg.gpsProcessingMethod", String.class); @@ -1487,6 +1676,7 @@ public final class CaptureResult extends CameraMetadata { /** * <p>Time GPS fix was made to include in * EXIF</p> + * @hide */ public static final Key<Long> JPEG_GPS_TIMESTAMP = new Key<Long>("android.jpeg.gpsTimestamp", long.class); @@ -1519,9 +1709,15 @@ public final class CaptureResult extends CameraMetadata { * 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> + * <p>If the thumbnail image aspect ratio differs from the JPEG primary image aspect + * ratio, the camera device creates the thumbnail by cropping it from the primary image. + * For example, if the primary image has 4:3 aspect ratio, the thumbnail image has + * 16:9 aspect ratio, the primary image will be cropped vertically (letterbox) to + * generate the thumbnail image. The thumbnail image will always have a smaller Field + * Of View (FOV) than the primary image when aspect ratios differ.</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); + public static final Key<android.util.Size> JPEG_THUMBNAIL_SIZE = + new Key<android.util.Size>("android.jpeg.thumbnailSize", android.util.Size.class); /** * <p>The ratio of lens focal length to the effective @@ -1608,8 +1804,8 @@ public final class CaptureResult extends CameraMetadata { * <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); + public static final Key<android.util.Pair<Float,Float>> LENS_FOCUS_RANGE = + new Key<android.util.Pair<Float,Float>>("android.lens.focusRange", new TypeReference<android.util.Pair<Float,Float>>() {{ }}); /** * <p>Sets whether the camera device uses optical image stabilization (OIS) @@ -1698,8 +1894,10 @@ public final class CaptureResult extends CameraMetadata { * capture must arrive before the FINAL buffer for that capture. This entry may * 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> + * @deprecated * @hide */ + @Deprecated public static final Key<Boolean> QUIRKS_PARTIAL_RESULT = new Key<Boolean>("android.quirks.partialResult", boolean.class); @@ -1743,8 +1941,11 @@ public final class CaptureResult extends CameraMetadata { * 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>The crop region is applied after the RAW to other color space (e.g. YUV) + * conversion. Since raw streams (e.g. RAW16) don't have the conversion stage, + * it is not croppable. The crop region will be ignored by raw streams.</p> + * <p>For non-raw streams, any additional per-stream cropping will + * 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 @@ -1819,7 +2020,8 @@ public final class CaptureResult extends CameraMetadata { * 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. + * is provided via the {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} field + * using StreamConfigurationMap#getOutputMinFrameDuration(int, Size). * 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 @@ -1829,7 +2031,8 @@ public final class CaptureResult extends CameraMetadata { * <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 + * looking it up in {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP android.scaler.streamConfigurationMap} using + * StreamConfigurationMap#getOutputMinFrameDuration(int, Size) (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 @@ -1837,7 +2040,8 @@ public final class CaptureResult extends CameraMetadata { * 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 + * StreamConfigurationMap#getOutputStallDuration(int,Size) using its + * respective size/format), 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> @@ -1848,10 +2052,9 @@ public final class CaptureResult extends CameraMetadata { * 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> + * StreamConfigurationMap#getOutputStallDuration(int,Size).</p> * - * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP */ public static final Key<Long> SENSOR_FRAME_DURATION = new Key<Long>("android.sensor.frameDuration", long.class); @@ -1877,21 +2080,6 @@ public final class CaptureResult extends CameraMetadata { 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><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> - * - * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL - */ - public static final Key<Float> SENSOR_TEMPERATURE = - new Key<Float>("android.sensor.temperature", float.class); - - /** * <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 @@ -1984,8 +2172,8 @@ public final class CaptureResult extends CameraMetadata { * <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 + * shading map with size specified as <code>android.lens.info.shadingMapSize = [ 4, 3 ]</code>, + * the output 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, @@ -1997,11 +2185,17 @@ public final class CaptureResult extends CameraMetadata { * <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> + * lens shading map data in android.statistics.lensShadingMap, with size specified + * by android.lens.info.shadingMapSize; the returned shading map data will be the one + * applied by the camera device for this capture request.</p> + * <p>The shading map data may depend on the AE and AWB statistics, therefore the reliability + * of the map data may be affected by the AE and AWB algorithms. When AE and AWB are in + * AUTO modes({@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} <code>!=</code> OFF and {@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} <code>!=</code> OFF), + * to get best results, it is recommended that the applications wait for the AE and AWB to + * be converged before using the returned shading map data.</p> * - * @see CameraCharacteristics#LENS_INFO_SHADING_MAP_SIZE - * @see CaptureResult#STATISTICS_LENS_SHADING_MAP + * @see CaptureRequest#CONTROL_AE_MODE + * @see CaptureRequest#CONTROL_AWB_MODE * @see CaptureRequest#STATISTICS_LENS_SHADING_MAP_MODE * @see #SHADING_MODE_OFF * @see #SHADING_MODE_FAST @@ -2064,6 +2258,62 @@ public final class CaptureResult extends CameraMetadata { new Key<byte[]>("android.statistics.faceScores", byte[].class); /** + * <p>List of the faces detected through camera face detection + * in this result.</p> + * <p>Only available if {@link CaptureRequest#STATISTICS_FACE_DETECT_MODE android.statistics.faceDetectMode} <code>!=</code> OFF.</p> + * + * @see CaptureRequest#STATISTICS_FACE_DETECT_MODE + */ + public static final Key<android.hardware.camera2.params.Face[]> STATISTICS_FACES = + new Key<android.hardware.camera2.params.Face[]>("android.statistics.faces", android.hardware.camera2.params.Face[].class); + + /** + * <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.</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>width,height = [ 4, 3 ] + * values = + * [ 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 + */ + public static final Key<android.hardware.camera2.params.LensShadingMap> STATISTICS_LENS_SHADING_CORRECTION_MAP = + new Key<android.hardware.camera2.params.LensShadingMap>("android.statistics.lensShadingCorrectionMap", android.hardware.camera2.params.LensShadingMap.class); + + /** * <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> @@ -2082,12 +2332,12 @@ public final class CaptureResult extends CameraMetadata { * <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> + * is provided in the camera static metadata by 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} = + * <pre><code>android.lens.info.shadingMapSize = [ 4, 3 ] + * 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, @@ -2106,8 +2356,7 @@ public final class CaptureResult extends CameraMetadata { * <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 + * @hide */ public static final Key<float[]> STATISTICS_LENS_SHADING_MAP = new Key<float[]>("android.statistics.lensShadingMap", float[].class); @@ -2126,8 +2375,10 @@ public final class CaptureResult extends CameraMetadata { * <p><b>Optional</b> - This value may be {@code null} on some devices.</p> * * @see CaptureRequest#COLOR_CORRECTION_GAINS + * @deprecated * @hide */ + @Deprecated public static final Key<float[]> STATISTICS_PREDICTED_COLOR_GAINS = new Key<float[]>("android.statistics.predictedColorGains", float[].class); @@ -2148,8 +2399,10 @@ public final class CaptureResult extends CameraMetadata { * <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> + * @deprecated * @hide */ + @Deprecated public static final Key<Rational[]> STATISTICS_PREDICTED_COLOR_TRANSFORM = new Key<Rational[]>("android.statistics.predictedColorTransform", Rational[].class); @@ -2203,17 +2456,15 @@ public final class CaptureResult extends CameraMetadata { * @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); + public static final Key<android.graphics.Point[]> STATISTICS_HOT_PIXEL_MAP = + new Key<android.graphics.Point[]>("android.statistics.hotPixelMap", android.graphics.Point[].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 + * 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 */ @@ -2224,10 +2475,10 @@ public final class CaptureResult extends CameraMetadata { * <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> + * <p>See android.tonemap.curveRed for more details.</p> * - * @see CaptureRequest#TONEMAP_CURVE_RED * @see CaptureRequest#TONEMAP_MODE + * @hide */ public static final Key<float[]> TONEMAP_CURVE_BLUE = new Key<float[]>("android.tonemap.curveBlue", float[].class); @@ -2236,10 +2487,10 @@ public final class CaptureResult extends CameraMetadata { * <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> + * <p>See android.tonemap.curveRed for more details.</p> * - * @see CaptureRequest#TONEMAP_CURVE_RED * @see CaptureRequest#TONEMAP_MODE + * @hide */ public static final Key<float[]> TONEMAP_CURVE_GREEN = new Key<float[]>("android.tonemap.curveGreen", float[].class); @@ -2249,7 +2500,7 @@ public final class CaptureResult extends CameraMetadata { * 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} = + * <pre><code>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 @@ -2265,15 +2516,15 @@ public final class CaptureResult extends CameraMetadata { * 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 ] + * <pre><code>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 ] + * <pre><code>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} = [ + * <pre><code>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, @@ -2281,7 +2532,7 @@ public final class CaptureResult extends CameraMetadata { * </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} = [ + * <pre><code>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, @@ -2289,14 +2540,67 @@ public final class CaptureResult extends CameraMetadata { * </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 + * @hide */ public static final Key<float[]> TONEMAP_CURVE_RED = new Key<float[]>("android.tonemap.curveRed", float[].class); /** + * <p>Tonemapping / contrast / gamma curve to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} + * is CONTRAST_CURVE.</p> + * <p>The tonemapCurve consist of three curves for each of red, green, and blue + * channels respectively. The following example uses the red channel as an + * example. The same logic applies to green and blue channel. + * Each channel's curve is defined by an array of control points:</p> + * <pre><code>curveRed = + * [ P0(in, out), P1(in, out), P2(in, out), P3(in, out), ..., PN(in, out) ] + * 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>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>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>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>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 CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS + * @see CaptureRequest#TONEMAP_MODE + */ + public static final Key<android.hardware.camera2.params.TonemapCurve> TONEMAP_CURVE = + new Key<android.hardware.camera2.params.TonemapCurve>("android.tonemap.curve", android.hardware.camera2.params.TonemapCurve.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 @@ -2312,18 +2616,15 @@ public final class CaptureResult extends CameraMetadata { * <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}. + * emit its own tonemap curve in {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}. * 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 + * <p>If a request is sent with CONTRAST_CURVE 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_CURVE * @see CaptureRequest#TONEMAP_MODE * @see #TONEMAP_MODE_CONTRAST_CURVE * @see #TONEMAP_MODE_FAST @@ -2425,19 +2726,4 @@ public final class CaptureResult extends CameraMetadata { /*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * End generated code *~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/ - - /** - * <p> - * List of the {@link Face Faces} detected through camera face detection - * in this result. - * </p> - * <p> - * Only available if {@link #STATISTICS_FACE_DETECT_MODE} {@code !=} - * {@link CameraMetadata#STATISTICS_FACE_DETECT_MODE_OFF OFF}. - * </p> - * - * @see Face - */ - public static final Key<Face[]> STATISTICS_FACES = - new Key<Face[]>("android.statistics.faces", Face[].class); } diff --git a/core/java/android/hardware/camera2/DngCreator.java b/core/java/android/hardware/camera2/DngCreator.java new file mode 100644 index 0000000..54568ed --- /dev/null +++ b/core/java/android/hardware/camera2/DngCreator.java @@ -0,0 +1,368 @@ +/* + * 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.hardware.camera2; + +import android.graphics.Bitmap; +import android.graphics.ImageFormat; +import android.hardware.camera2.impl.CameraMetadataNative; +import android.location.Location; +import android.media.ExifInterface; +import android.media.Image; +import android.util.Size; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * The {@link DngCreator} class provides functions to write raw pixel data as a DNG file. + * + * <p> + * This class is designed to be used with the {@link android.graphics.ImageFormat#RAW_SENSOR} + * buffers available from {@link android.hardware.camera2.CameraDevice}, or with Bayer-type raw + * pixel data that is otherwise generated by an application. The DNG metadata tags will be + * generated from a {@link android.hardware.camera2.CaptureResult} object or set directly. + * </p> + * + * <p> + * The DNG file format is a cross-platform file format that is used to store pixel data from + * camera sensors with minimal pre-processing applied. DNG files allow for pixel data to be + * defined in a user-defined colorspace, and have associated metadata that allow for this + * pixel data to be converted to the standard CIE XYZ colorspace during post-processing. + * </p> + * + * <p> + * For more information on the DNG file format and associated metadata, please refer to the + * <a href= + * "https://wwwimages2.adobe.com/content/dam/Adobe/en/products/photoshop/pdfs/dng_spec_1.4.0.0.pdf"> + * Adobe DNG 1.4.0.0 specification</a>. + * </p> + */ +public final class DngCreator implements AutoCloseable { + + /** + * Create a new DNG object. + * + * <p> + * It is not necessary to call any set methods to write a well-formatted DNG file. + * </p> + * <p> + * DNG metadata tags will be generated from the corresponding parameters in the + * {@link android.hardware.camera2.CaptureResult} object. This removes or overrides + * all previous tags set. + * </p> + * + * @param characteristics an object containing the static + * {@link android.hardware.camera2.CameraCharacteristics}. + * @param metadata a metadata object to generate tags from. + */ + public DngCreator(CameraCharacteristics characteristics, CaptureResult metadata) { + if (characteristics == null || metadata == null) { + throw new NullPointerException("Null argument to DngCreator constructor"); + } + nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy()); + } + + /** + * Set the orientation value to write. + * + * <p> + * This will be written as the TIFF "Orientation" tag {@code (0x0112)}. + * Calling this will override any prior settings for this tag. + * </p> + * + * @param orientation the orientation value to set, one of: + * <ul> + * <li>{@link android.media.ExifInterface#ORIENTATION_NORMAL}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_HORIZONTAL}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_180}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_FLIP_VERTICAL}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSPOSE}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_90}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_TRANSVERSE}</li> + * <li>{@link android.media.ExifInterface#ORIENTATION_ROTATE_270}</li> + * </ul> + * @return this {@link #DngCreator} object. + */ + public DngCreator setOrientation(int orientation) { + + if (orientation < ExifInterface.ORIENTATION_UNDEFINED || + orientation > ExifInterface.ORIENTATION_ROTATE_270) { + throw new IllegalArgumentException("Orientation " + orientation + + " is not a valid EXIF orientation value"); + } + nativeSetOrientation(orientation); + return this; + } + + /** + * Set the thumbnail image. + * + * <p> + * Pixel data will be converted to a Baseline TIFF RGB image, with 8 bits per color channel. + * The alpha channel will be discarded. + * </p> + * + * <p> + * The given bitmap should not be altered while this object is in use. + * </p> + * + * @param pixels a {@link android.graphics.Bitmap} of pixel data. + * @return this {@link #DngCreator} object. + */ + public DngCreator setThumbnail(Bitmap pixels) { + if (pixels == null) { + throw new NullPointerException("Null argument to setThumbnail"); + } + + Bitmap.Config config = pixels.getConfig(); + + if (config != Bitmap.Config.ARGB_8888) { + pixels = pixels.copy(Bitmap.Config.ARGB_8888, false); + if (pixels == null) { + throw new IllegalArgumentException("Unsupported Bitmap format " + config); + } + nativeSetThumbnailBitmap(pixels); + } + + return this; + } + + /** + * Set the thumbnail image. + * + * <p> + * Pixel data is interpreted as a {@link android.graphics.ImageFormat#YUV_420_888} image. + * </p> + * + * <p> + * The given image should not be altered while this object is in use. + * </p> + * + * @param pixels an {@link android.media.Image} object with the format + * {@link android.graphics.ImageFormat#YUV_420_888}. + * @return this {@link #DngCreator} object. + */ + public DngCreator setThumbnail(Image pixels) { + if (pixels == null) { + throw new NullPointerException("Null argument to setThumbnail"); + } + + int format = pixels.getFormat(); + if (format != ImageFormat.YUV_420_888) { + throw new IllegalArgumentException("Unsupported image format " + format); + } + + Image.Plane[] planes = pixels.getPlanes(); + nativeSetThumbnailImage(pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(), + planes[0].getRowStride(), planes[0].getPixelStride(), planes[1].getBuffer(), + planes[1].getRowStride(), planes[1].getPixelStride(), planes[1].getBuffer(), + planes[1].getRowStride(), planes[1].getPixelStride()); + + return this; + } + + + /** + * Set image location metadata. + * + * <p> + * The given location object must contain at least a valid time, latitude, and longitude + * (equivalent to the values returned by {@link android.location.Location#getTime()}, + * {@link android.location.Location#getLatitude()}, and + * {@link android.location.Location#getLongitude()} methods). + * </p> + * + * @param location an {@link android.location.Location} object to set. + * @return this {@link #DngCreator} object. + * + * @throws java.lang.IllegalArgumentException if the given location object doesn't + * contain enough information to set location metadata. + */ + public DngCreator setLocation(Location location) { + /*TODO*/ + return this; + } + + /** + * Set the user description string to write. + * + * <p> + * This is equivalent to setting the TIFF "ImageDescription" tag {@code (0x010E)}. + * </p> + * + * @param description the user description string. + * @return this {@link #DngCreator} object. + */ + public DngCreator setDescription(String description) { + /*TODO*/ + return this; + } + + /** + * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with + * the currently configured metadata. + * + * <p> + * Raw pixel data must have 16 bits per pixel, and the input must contain at least + * {@code offset + 2 * width * height)} bytes. The width and height of + * the input are taken from the width and height set in the {@link DngCreator} metadata tags, + * and will typically be equal to the width and height of + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. + * The pixel layout in the input is determined from the reported color filter arrangement (CFA) + * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient + * metadata is available to write a well-formatted DNG file, an + * {@link java.lang.IllegalStateException} will be thrown. + * </p> + * + * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. + * @param size the {@link Size} of the image to write, in pixels. + * @param pixels an {@link java.io.InputStream} of pixel data to write. + * @param offset the offset of the raw image in bytes. This indicates how many bytes will + * be skipped in the input before any pixel data is read. + * + * @throws IOException if an error was encountered in the input or output stream. + * @throws java.lang.IllegalStateException if not enough metadata information has been + * set to write a well-formatted DNG file. + * @throws java.lang.IllegalArgumentException if the size passed in does not match the + */ + public void writeInputStream(OutputStream dngOutput, Size size, InputStream pixels, long offset) + throws IOException { + if (dngOutput == null || pixels == null) { + throw new NullPointerException("Null argument to writeImage"); + } + nativeWriteInputStream(dngOutput, pixels, offset); + } + + /** + * Write the {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data to a DNG file with + * the currently configured metadata. + * + * <p> + * Raw pixel data must have 16 bits per pixel, and the input must contain at least + * {@code offset + 2 * width * height)} bytes. The width and height of + * the input are taken from the width and height set in the {@link DngCreator} metadata tags, + * and will typically be equal to the width and height of + * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE}. + * The pixel layout in the input is determined from the reported color filter arrangement (CFA) + * set in {@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT}. If insufficient + * metadata is available to write a well-formatted DNG file, an + * {@link java.lang.IllegalStateException} will be thrown. + * </p> + * + * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. + * @param size the {@link Size} of the image to write, in pixels. + * @param pixels an {@link java.nio.ByteBuffer} of pixel data to write. + * @param offset the offset of the raw image in bytes. This indicates how many bytes will + * be skipped in the input before any pixel data is read. + * + * @throws IOException if an error was encountered in the input or output stream. + * @throws java.lang.IllegalStateException if not enough metadata information has been + * set to write a well-formatted DNG file. + */ + public void writeByteBuffer(OutputStream dngOutput, Size size, ByteBuffer pixels, long offset) + throws IOException { + if (dngOutput == null || pixels == null) { + throw new NullPointerException("Null argument to writeImage"); + } + nativeWriteByteBuffer(dngOutput, pixels, offset); + } + + /** + * Write the pixel data to a DNG file with the currently configured metadata. + * + * <p> + * For this method to succeed, the {@link android.media.Image} input must contain + * {@link android.graphics.ImageFormat#RAW_SENSOR} pixel data, otherwise an + * {@link java.lang.IllegalArgumentException} will be thrown. + * </p> + * + * @param dngOutput an {@link java.io.OutputStream} to write the DNG file to. + * @param pixels an {@link android.media.Image} to write. + * + * @throws java.io.IOException if an error was encountered in the output stream. + * @throws java.lang.IllegalArgumentException if an image with an unsupported format was used. + * @throws java.lang.IllegalStateException if not enough metadata information has been + * set to write a well-formatted DNG file. + */ + public void writeImage(OutputStream dngOutput, Image pixels) throws IOException { + if (dngOutput == null || pixels == null) { + throw new NullPointerException("Null argument to writeImage"); + } + + int format = pixels.getFormat(); + if (format != ImageFormat.RAW_SENSOR) { + throw new IllegalArgumentException("Unsupported image format " + format); + } + + Image.Plane[] planes = pixels.getPlanes(); + nativeWriteImage(dngOutput, pixels.getWidth(), pixels.getHeight(), planes[0].getBuffer(), + planes[0].getRowStride(), planes[0].getPixelStride()); + } + + @Override + public void close() { + nativeDestroy(); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * This field is used by native code, do not access or modify. + */ + private long mNativeContext; + + private static native void nativeClassInit(); + + private synchronized native void nativeInit(CameraMetadataNative nativeCharacteristics, + CameraMetadataNative nativeResult); + + private synchronized native void nativeDestroy(); + + private synchronized native void nativeSetOrientation(int orientation); + + private synchronized native void nativeSetThumbnailBitmap(Bitmap bitmap); + + private synchronized native void nativeSetThumbnailImage(int width, int height, + ByteBuffer yBuffer, int yRowStride, + int yPixStride, ByteBuffer uBuffer, + int uRowStride, int uPixStride, + ByteBuffer vBuffer, int vRowStride, + int vPixStride); + + private synchronized native void nativeWriteImage(OutputStream out, int width, int height, + ByteBuffer rawBuffer, int rowStride, + int pixStride) throws IOException; + + private synchronized native void nativeWriteByteBuffer(OutputStream out, ByteBuffer rawBuffer, + long offset) throws IOException; + + private synchronized native void nativeWriteInputStream(OutputStream out, InputStream rawStream, + long offset) throws IOException; + + static { + nativeClassInit(); + } +} diff --git a/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl b/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl index a14d38b..caabed3 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceCallbacks.aidl @@ -17,7 +17,7 @@ package android.hardware.camera2; import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.CaptureResultExtras; +import android.hardware.camera2.impl.CaptureResultExtras; /** @hide */ interface ICameraDeviceCallbacks diff --git a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl index d77f3d1..50a58ed 100644 --- a/core/java/android/hardware/camera2/ICameraDeviceUser.aidl +++ b/core/java/android/hardware/camera2/ICameraDeviceUser.aidl @@ -20,13 +20,14 @@ import android.view.Surface; import android.hardware.camera2.impl.CameraMetadataNative; import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.LongParcelable; +import android.hardware.camera2.utils.LongParcelable; /** @hide */ interface ICameraDeviceUser { /** - * Keep up-to-date with frameworks/av/include/camera/camera2/ICameraDeviceUser.h + * Keep up-to-date with frameworks/av/include/camera/camera2/ICameraDeviceUser.h and + * frameworks/base/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java */ void disconnect(); @@ -41,6 +42,27 @@ interface ICameraDeviceUser int cancelRequest(int requestId, out LongParcelable lastFrameNumber); + /** + * Begin the device configuration. + * + * <p> + * beginConfigure must be called before any call to deleteStream, createStream, + * or endConfigure. It is not valid to call this when the device is not idle. + * <p> + */ + int beginConfigure(); + + /** + * End the device configuration. + * + * <p> + * endConfigure must be called after stream configuration is complete (i.e. after + * a call to beginConfigure and subsequent createStream/deleteStream calls). This + * must be called before any requests can be submitted. + * <p> + */ + int endConfigure(); + int deleteStream(int streamId); // non-negative value is the stream ID. negative value is status_t diff --git a/core/java/android/hardware/camera2/Size.java b/core/java/android/hardware/camera2/Size.java deleted file mode 100644 index 9328a003..0000000 --- a/core/java/android/hardware/camera2/Size.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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.hardware.camera2; - -// TODO: Delete this class, since it was moved to android.util as public API - -/** - * Immutable class for describing width and height dimensions in pixels. - * - * @hide - */ -public final class Size { - /** - * Create a new immutable Size instance. - * - * @param width The width of the size, in pixels - * @param height The height of the size, in pixels - */ - public Size(final int width, final int height) { - mWidth = width; - mHeight = height; - } - - /** - * Get the width of the size (in pixels). - * @return width - */ - public final int getWidth() { - return mWidth; - } - - /** - * Get the height of the size (in pixels). - * @return height - */ - public final int getHeight() { - return mHeight; - } - - /** - * Check if this size is equal to another size. - * <p> - * Two sizes are equal if and only if both their widths and heights are - * equal. - * </p> - * <p> - * A size object is never equal to any other type of object. - * </p> - * - * @return {@code true} if the objects were equal, {@code false} otherwise - */ - @Override - public boolean equals(final Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj instanceof Size) { - final Size other = (Size) obj; - return mWidth == other.mWidth && mHeight == other.mHeight; - } - return false; - } - - /** - * Return the size represented as a string with the format {@code "WxH"} - * - * @return string representation of the size - */ - @Override - public String toString() { - return mWidth + "x" + mHeight; - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - // assuming most sizes are <2^16, doing a rotate will give us perfect hashing - return mHeight ^ ((mWidth << (Integer.SIZE / 2)) | (mWidth >>> (Integer.SIZE / 2))); - } - - private final int mWidth; - private final int mHeight; -}; diff --git a/core/java/android/hardware/camera2/StreamConfigurationMap.java b/core/java/android/hardware/camera2/StreamConfigurationMap.java deleted file mode 100644 index e24fd1b..0000000 --- a/core/java/android/hardware/camera2/StreamConfigurationMap.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * 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.graphics.ImageFormat; -import android.graphics.PixelFormat; -import android.hardware.camera2.impl.HashCodeHelpers; -import android.view.Surface; -import android.util.Size; - -import java.util.Arrays; - -import static com.android.internal.util.Preconditions.*; - -/** - * Immutable class to store the available stream - * {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS configurations} to be used - * when configuring streams with {@link CameraDevice#configureOutputs}. - * <!-- TODO: link to input stream configuration --> - * - * <p>This is the authoritative list for all <!-- input/ -->output formats (and sizes respectively - * for that format) that are supported by a camera device.</p> - * - * <p>This also contains the minimum frame durations and stall durations for each format/size - * combination that can be used to calculate effective frame rate when submitting multiple captures. - * </p> - * - * <p>An instance of this object is available from {@link CameraCharacteristics} using - * the {@link CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS} key and the - * {@link CameraCharacteristics#get} method.</p. - * - * <pre>{@code - * CameraCharacteristics characteristics = ...; - * StreamConfigurationMap configs = characteristics.get( - * CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS); - * }</pre> - * - * @see CameraCharacteristics#SCALER_AVAILABLE_STREAM_CONFIGURATIONS - * @see CameraDevice#configureOutputs - */ -public final class StreamConfigurationMap { - - /** - * Create a new {@link StreamConfigurationMap}. - * - * <p>The array parameters ownership is passed to this object after creation; do not - * write to them after this constructor is invoked.</p> - * - * @param configurations a non-{@code null} array of {@link StreamConfiguration} - * @param durations a non-{@code null} array of {@link StreamConfigurationDuration} - * - * @throws NullPointerException if any of the arguments or subelements were {@code null} - * - * @hide - */ - public StreamConfigurationMap( - StreamConfiguration[] configurations, - StreamConfigurationDuration[] durations) { - // TODO: format check against ImageFormat/PixelFormat ? - - mConfigurations = checkArrayElementsNotNull(configurations, "configurations"); - mDurations = checkArrayElementsNotNull(durations, "durations"); - - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the image {@code format} output formats in this stream configuration. - * - * <p>All image formats returned by this function will be defined in either {@link ImageFormat} - * or in {@link PixelFormat} (and there is no possibility of collision).</p> - * - * <p>Formats listed in this array are guaranteed to return true if queried with - * {@link #isOutputSupportedFor(int).</p> - * - * @return an array of integer format - * - * @see ImageFormat - * @see PixelFormat - */ - public final int[] getOutputFormats() { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the image {@code format} input formats in this stream configuration. - * - * <p>All image formats returned by this function will be defined in either {@link ImageFormat} - * or in {@link PixelFormat} (and there is no possibility of collision).</p> - * - * @return an array of integer format - * - * @see ImageFormat - * @see PixelFormat - * - * @hide - */ - public final int[] getInputFormats() { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the supported input sizes for this input format. - * - * <p>The format must have come from {@link #getInputFormats}; otherwise - * {@code null} is returned.</p> - * - * @param format a format from {@link #getInputFormats} - * @return a non-empty array of sizes, or {@code null} if the format was not available. - * - * @hide - */ - public Size[] getInputSizes(final int format) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Determine whether or not output streams can be - * {@link CameraDevice#configureOutputs configured} with a particular user-defined format. - * - * <p>This method determines that the output {@code format} is supported by the camera device; - * each output {@code surface} target may or may not itself support that {@code format}. - * Refer to the class which provides the surface for additional documentation.</p> - * - * <p>Formats for which this returns {@code true} are guaranteed to exist in the result - * returned by {@link #getOutputSizes}.</p> - * - * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} - * @return - * {@code true} iff using a {@code surface} with this {@code format} will be - * supported with {@link CameraDevice#configureOutputs} - * - * @throws IllegalArgumentException - * if the image format was not a defined named constant - * from either {@link ImageFormat} or {@link PixelFormat} - * - * @see ImageFormat - * @see PixelFormat - * @see CameraDevice#configureOutputs - */ - public boolean isOutputSupportedFor(int format) { - checkArgumentFormat(format); - - final int[] formats = getOutputFormats(); - for (int i = 0; i < formats.length; ++i) { - if (format == formats[i]) { - return true; - } - } - - return false; - } - - /** - * Determine whether or not output streams can be configured with a particular class - * as a consumer. - * - * <p>The following list is generally usable for outputs: - * <ul> - * <li>{@link android.media.ImageReader} - - * Recommended for image processing or streaming to external resources (such as a file or - * network) - * <li>{@link android.media.MediaRecorder} - - * Recommended for recording video (simple to use) - * <li>{@link android.media.MediaCodec} - - * Recommended for recording video (more complicated to use, with more flexibility) - * <li>{@link android.renderscript.Allocation} - - * Recommended for image processing with {@link android.renderscript RenderScript} - * <li>{@link android.view.SurfaceHolder} - - * Recommended for low-power camera preview with {@link android.view.SurfaceView} - * <li>{@link android.graphics.SurfaceTexture} - - * Recommended for OpenGL-accelerated preview processing or compositing with - * {@link android.view.TextureView} - * </ul> - * </p> - * - * <p>Generally speaking this means that creating a {@link Surface} from that class <i>may</i> - * provide a producer endpoint that is suitable to be used with - * {@link CameraDevice#configureOutputs}.</p> - * - * <p>Since not all of the above classes support output of all format and size combinations, - * the particular combination should be queried with {@link #isOutputSupportedFor(Surface)}.</p> - * - * @param klass a non-{@code null} {@link Class} object reference - * @return {@code true} if this class is supported as an output, {@code false} otherwise - * - * @throws NullPointerException if {@code klass} was {@code null} - * - * @see CameraDevice#configureOutputs - * @see #isOutputSupportedFor(Surface) - */ - public static <T> boolean isOutputSupportedFor(final Class<T> klass) { - checkNotNull(klass, "klass must not be null"); - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Determine whether or not the {@code surface} in its current state is suitable to be - * {@link CameraDevice#configureOutputs configured} as an output. - * - * <p>Not all surfaces are usable with the {@link CameraDevice}, and not all configurations - * of that {@code surface} are compatible. Some classes that provide the {@code surface} are - * compatible with the {@link CameraDevice} in general - * (see {@link #isOutputSupportedFor(Class)}, but it is the caller's responsibility to put the - * {@code surface} into a state that will be compatible with the {@link CameraDevice}.</p> - * - * <p>Reasons for a {@code surface} being specifically incompatible might be: - * <ul> - * <li>Using a format that's not listed by {@link #getOutputFormats} - * <li>Using a format/size combination that's not listed by {@link #getOutputSizes} - * <li>The {@code surface} itself is not in a state where it can service a new producer.</p> - * </li> - * </ul> - * - * This is not an exhaustive list; see the particular class's documentation for further - * possible reasons of incompatibility.</p> - * - * @param surface a non-{@code null} {@link Surface} object reference - * @return {@code true} if this is supported, {@code false} otherwise - * - * @throws NullPointerException if {@code surface} was {@code null} - * - * @see CameraDevice#configureOutputs - * @see #isOutputSupportedFor(Class) - */ - public boolean isOutputSupportedFor(final Surface surface) { - checkNotNull(surface, "surface must not be null"); - - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get a list of sizes compatible with {@code klass} to use as an output. - * - * <p>Since some of the supported classes may support additional formats beyond - * an opaque/implementation-defined (under-the-hood) format; this function only returns - * sizes for the implementation-defined format.</p> - * - * <p>Some classes such as {@link android.media.ImageReader} may only support user-defined - * formats; in particular {@link #isOutputSupportedFor(Class)} will return {@code true} for - * that class and this method will return an empty array (but not {@code null}).</p> - * - * <p>If a well-defined format such as {@code NV21} is required, use - * {@link #getOutputSizes(int)} instead.</p> - * - * <p>The {@code klass} should be a supported output, that querying - * {@code #isOutputSupportedFor(Class)} should return {@code true}.</p> - * - * @param klass - * a non-{@code null} {@link Class} object reference - * @return - * an array of supported sizes for implementation-defined formats, - * or {@code null} iff the {@code klass} is not a supported output - * - * @throws NullPointerException if {@code klass} was {@code null} - * - * @see #isOutputSupportedFor(Class) - */ - public <T> Size[] getOutputSizes(final Class<T> klass) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get a list of sizes compatible with the requested image {@code format}. - * - * <p>The {@code format} should be a supported format (one of the formats returned by - * {@link #getOutputFormats}).</p> - * - * @param format an image format from {@link ImageFormat} or {@link PixelFormat} - * @return - * an array of supported sizes, - * or {@code null} if the {@code format} is not a supported output - * - * @see ImageFormat - * @see PixelFormat - * @see #getOutputFormats - */ - public Size[] getOutputSizes(final int format) { - try { - checkArgumentFormatSupported(format, /*output*/true); - } catch (IllegalArgumentException e) { - return null; - } - - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the minimum {@link CaptureRequest#SENSOR_FRAME_DURATION frame duration} - * for the format/size combination (in nanoseconds). - * - * <p>{@code format} should be one of the ones returned by {@link #getOutputFormats()}.</p> - * <p>{@code size} should be one of the ones returned by - * {@link #getOutputSizes(int)}.</p> - * - * @param format an image format from {@link ImageFormat} or {@link PixelFormat} - * @param size an output-compatible size - * @return a minimum frame duration {@code >=} 0 in nanoseconds - * - * @throws IllegalArgumentException if {@code format} or {@code size} was not supported - * @throws NullPointerException if {@code size} was {@code null} - * - * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS - * @see CaptureRequest#SENSOR_FRAME_DURATION - * @see ImageFormat - * @see PixelFormat - */ - public long getOutputMinFrameDuration(final int format, final Size size) { - checkArgumentFormatSupported(format, /*output*/true); - - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the minimum {@link CaptureRequest#SENSOR_FRAME_DURATION frame duration} - * for the class/size combination (in nanoseconds). - * - * <p>This assumes a the {@code klass} is set up to use an implementation-defined format. - * For user-defined formats, use {@link #getOutputMinFrameDuration(int, Size)}.</p> - * - * <p>{@code klass} should be one of the ones which is supported by - * {@link #isOutputSupportedFor(Class)}.</p> - * - * <p>{@code size} should be one of the ones returned by - * {@link #getOutputSizes(int)}.</p> - * - * @param klass - * a class which is supported by {@link #isOutputSupportedFor(Class)} and has a - * non-empty array returned by {@link #getOutputSizes(Class)} - * @param size an output-compatible size - * @return a minimum frame duration {@code >=} 0 in nanoseconds - * - * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported - * @throws NullPointerException if {@code size} or {@code klass} was {@code null} - * - * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS - * @see CaptureRequest#SENSOR_FRAME_DURATION - * @see ImageFormat - * @see PixelFormat - */ - public <T> long getOutputMinFrameDuration(final Class<T> klass, final Size size) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS stall duration} - * for the format/size combination (in nanoseconds). - * - * <p>{@code format} should be one of the ones returned by {@link #getOutputFormats()}.</p> - * <p>{@code size} should be one of the ones returned by - * {@link #getOutputSizes(int)}.</p> - * - * @param format an image format from {@link ImageFormat} or {@link PixelFormat} - * @param size an output-compatible size - * @return a stall duration {@code >=} 0 in nanoseconds - * - * @throws IllegalArgumentException if {@code format} or {@code size} was not supported - * @throws NullPointerException if {@code size} was {@code null} - * - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS - * @see ImageFormat - * @see PixelFormat - */ - public long getOutputStallDuration(final int format, final Size size) { - checkArgumentFormatSupported(format, /*output*/true); - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Get the {@link CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS stall duration} - * for the class/size combination (in nanoseconds). - * - * <p>This assumes a the {@code klass} is set up to use an implementation-defined format. - * For user-defined formats, use {@link #getOutputMinFrameDuration(int, Size)}.</p> - * - * <p>{@code klass} should be one of the ones with a non-empty array returned by - * {@link #getOutputSizes(Class)}.</p> - * - * <p>{@code size} should be one of the ones returned by - * {@link #getOutputSizes(Class)}.</p> - * - * @param klass - * a class which is supported by {@link #isOutputSupportedFor(Class)} and has a - * non-empty array returned by {@link #getOutputSizes(Class)} - * @param size an output-compatible size - * @return a minimum frame duration {@code >=} 0 in nanoseconds - * - * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported - * @throws NullPointerException if {@code size} or {@code klass} was {@code null} - * - * @see CameraCharacteristics#SCALER_AVAILABLE_MIN_FRAME_DURATIONS - * @see CaptureRequest#SENSOR_FRAME_DURATION - * @see ImageFormat - * @see PixelFormat - */ - public <T> long getOutputStallDuration(final Class<T> klass, final Size size) { - throw new UnsupportedOperationException("Not implemented yet"); - } - - /** - * Check if this {@link StreamConfigurationMap} is equal to another - * {@link StreamConfigurationMap}. - * - * <p>Two vectors are only equal if and only if each of the respective elements is equal.</p> - * - * @return {@code true} if the objects were equal, {@code false} otherwise - */ - @Override - public boolean equals(final Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (obj instanceof StreamConfigurationMap) { - final StreamConfigurationMap other = (StreamConfigurationMap) obj; - // TODO: do we care about order? - return Arrays.equals(mConfigurations, other.mConfigurations) && - Arrays.equals(mDurations, other.mDurations); - } - return false; - } - - /** - * {@inheritDoc} - */ - @Override - public int hashCode() { - // TODO: do we care about order? - return HashCodeHelpers.hashCode(mConfigurations) ^ HashCodeHelpers.hashCode(mDurations); - } - - // Check that the argument is supported by #getOutputFormats or #getInputFormats - private int checkArgumentFormatSupported(int format, boolean output) { - checkArgumentFormat(format); - - int[] formats = output ? getOutputFormats() : getInputFormats(); - for (int i = 0; i < formats.length; ++i) { - if (format == formats[i]) { - return format; - } - } - - throw new IllegalArgumentException(String.format( - "format %x is not supported by this stream configuration map", format)); - } - - /** - * Ensures that the format is either user-defined or implementation defined. - * - * <p>Any invalid/undefined formats will raise an exception.</p> - * - * @param format image format - * @return the format - * - * @throws IllegalArgumentException if the format was invalid - */ - static int checkArgumentFormatInternal(int format) { - if (format == HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) { - return format; - } - - return checkArgumentFormat(format); - } - - /** - * Ensures that the format is user-defined in either ImageFormat or PixelFormat. - * - * <p>Any invalid/undefined formats will raise an exception, including implementation-defined. - * </p> - * - * <p>Note that {@code @hide} and deprecated formats will not pass this check.</p> - * - * @param format image format - * @return the format - * - * @throws IllegalArgumentException if the format was not user-defined - */ - static int checkArgumentFormat(int format) { - if (!ImageFormat.isPublicFormat(format) && !PixelFormat.isPublicFormat(format)) { - throw new IllegalArgumentException(String.format( - "format %x was not defined in either ImageFormat or PixelFormat", format)); - } - - return format; - } - - private static final int HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED = 0x22; - - private final StreamConfiguration[] mConfigurations; - private final StreamConfigurationDuration[] mDurations; - -} diff --git a/core/java/android/hardware/camera2/TotalCaptureResult.java b/core/java/android/hardware/camera2/TotalCaptureResult.java new file mode 100644 index 0000000..2647959 --- /dev/null +++ b/core/java/android/hardware/camera2/TotalCaptureResult.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 android.hardware.camera2; + +import android.hardware.camera2.impl.CameraMetadataNative; + +import java.util.Collections; +import java.util.List; + +/** + * <p>The total assembled results of a single image capture from the image sensor.</p> + * + * <p>Contains the final configuration for the capture hardware (sensor, lens, + * flash), the processing pipeline, the control algorithms, and the output + * buffers.</p> + * + * <p>A {@code TotalCaptureResult} is produced by a {@link CameraDevice} after processing a + * {@link CaptureRequest}. All properties listed for capture requests can also + * be queried on the capture result, to determine the final values used for + * capture. The result also includes additional metadata about the state of the + * camera device during the capture.</p> + * + * <p>All properties returned by {@link CameraCharacteristics#getAvailableCaptureResultKeys()} + * are available (that is {@link CaptureResult#get} will return non-{@code null}, if and only if + * that key that was enabled by the request. A few keys such as + * {@link CaptureResult#STATISTICS_FACES} are disabled by default unless enabled with a switch (such + * as {@link CaptureRequest#STATISTICS_FACE_DETECT_MODE}). Refer to each key documentation on + * a case-by-case basis.</p> + * + * <p>{@link TotalCaptureResult} objects are immutable.</p> + * + * @see CameraDevice.CaptureListener#onCaptureCompleted + */ +public final class TotalCaptureResult extends CaptureResult { + + /** + * Takes ownership of the passed-in properties object + * @hide + */ + public TotalCaptureResult(CameraMetadataNative results, CaptureRequest parent, int sequenceId) { + super(results, parent, sequenceId); + } + + /** + * Creates a request-less result. + * + * <p><strong>For testing only.</strong></p> + * @hide + */ + public TotalCaptureResult(CameraMetadataNative results, int sequenceId) { + super(results, sequenceId); + } + + /** + * Get the read-only list of partial results that compose this total result. + * + * <p>The list is returned is unmodifiable; attempting to modify it will result in a + * {@code UnsupportedOperationException} being thrown.</p> + * + * <p>The list size will be inclusive between {@code 1} and + * {@link CameraCharacteristics#REQUEST_PARTIAL_RESULT_COUNT}, in ascending order + * of when {@link CameraDevice.CaptureListener#onCaptureProgressed} was invoked.</p> + * + * @return unmodifiable list of partial results + */ + public List<CaptureResult> getPartialResults() { + // TODO + return Collections.unmodifiableList(null); + } +} diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDevice.java index 988f8f9..9a4c531 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDevice.java @@ -19,14 +19,17 @@ package android.hardware.camera2.impl; import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE; import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; 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.TotalCaptureResult; import android.hardware.camera2.utils.CameraBinderDecorator; import android.hardware.camera2.utils.CameraRuntimeException; +import android.hardware.camera2.utils.LongParcelable; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -72,6 +75,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final SparseArray<Surface> mConfiguredOutputs = new SparseArray<Surface>(); private final String mCameraId; + private final CameraCharacteristics mCharacteristics; /** * A list tracking request and its expected last frame. @@ -150,13 +154,15 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } }; - public CameraDevice(String cameraId, StateListener listener, Handler handler) { + public CameraDevice(String cameraId, StateListener listener, Handler handler, + CameraCharacteristics characteristics) { if (cameraId == null || listener == null || handler == null) { throw new IllegalArgumentException("Null argument given"); } mCameraId = cameraId; mDeviceListener = listener; mDeviceHandler = handler; + mCharacteristics = characteristics; final int MAX_TAG_LEN = 23; String tag = String.format("CameraDevice-JV-%s", mCameraId); @@ -217,7 +223,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { try { waitUntilIdle(); - // TODO: mRemoteDevice.beginConfigure + mRemoteDevice.beginConfigure(); // Delete all streams first (to free up HW resources) for (Integer streamId : deleteList) { mRemoteDevice.deleteStream(streamId); @@ -232,7 +238,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { mConfiguredOutputs.put(streamId, s); } - // TODO: mRemoteDevice.endConfigure + mRemoteDevice.endConfigure(); } catch (CameraRuntimeException e) { if (e.getReason() == CAMERA_IN_USE) { throw new IllegalStateException("The camera is currently busy." + @@ -254,6 +260,13 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override + public void createCaptureSession(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler handler) + throws CameraAccessException { + // TODO + } + + @Override public CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException { synchronized (mLock) { @@ -352,7 +365,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { holder.getListener().onCaptureSequenceCompleted( CameraDevice.this, requestId, - (int)lastFrameNumber); + lastFrameNumber); } } }; @@ -710,7 +723,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { holder.getListener().onCaptureSequenceCompleted( CameraDevice.this, requestId, - (int)lastFrameNumber); + lastFrameNumber); } } }; @@ -843,11 +856,18 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public void onResultReceived(CameraMetadataNative result, CaptureResultExtras resultExtras) throws RemoteException { + int requestId = resultExtras.getRequestId(); if (DEBUG) { Log.v(TAG, "Received result frame " + resultExtras.getFrameNumber() + " for id " + requestId); } + + + // TODO: Handle CameraCharacteristics access from CaptureResult correctly. + result.set(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE, + getCharacteristics().get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE)); + final CaptureListenerHolder holder; synchronized (mLock) { holder = CameraDevice.this.mCaptureListenerMap.get(requestId); @@ -881,12 +901,15 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } final CaptureRequest request = holder.getRequest(resultExtras.getSubsequenceId()); - final CaptureResult resultAsCapture = new CaptureResult(result, request, requestId); + Runnable resultDispatch = null; // Either send a partial result or the final capture completed result if (quirkIsPartialResult) { + final CaptureResult resultAsCapture = + new CaptureResult(result, request, requestId); + // Partial result resultDispatch = new Runnable() { @Override @@ -900,6 +923,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } }; } else { + final TotalCaptureResult resultAsCapture = + new TotalCaptureResult(result, request, requestId); + // Final capture result resultDispatch = new Runnable() { @Override @@ -951,4 +977,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { return (mRemoteDevice == null); } } + + private CameraCharacteristics getCharacteristics() { + return mCharacteristics; + } } diff --git a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java index c5e5753..83aee5d 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -20,15 +20,47 @@ import android.graphics.ImageFormat; import android.graphics.Point; import android.graphics.Rect; import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; -import android.hardware.camera2.Face; -import android.hardware.camera2.Rational; +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.marshal.MarshalRegistry; +import android.hardware.camera2.marshal.impl.MarshalQueryableArray; +import android.hardware.camera2.marshal.impl.MarshalQueryableBoolean; +import android.hardware.camera2.marshal.impl.MarshalQueryableColorSpaceTransform; +import android.hardware.camera2.marshal.impl.MarshalQueryableEnum; +import android.hardware.camera2.marshal.impl.MarshalQueryableMeteringRectangle; +import android.hardware.camera2.marshal.impl.MarshalQueryableNativeByteToInteger; +import android.hardware.camera2.marshal.impl.MarshalQueryablePair; +import android.hardware.camera2.marshal.impl.MarshalQueryableParcelable; +import android.hardware.camera2.marshal.impl.MarshalQueryablePrimitive; +import android.hardware.camera2.marshal.impl.MarshalQueryableRange; +import android.hardware.camera2.marshal.impl.MarshalQueryableRect; +import android.hardware.camera2.marshal.impl.MarshalQueryableReprocessFormatsMap; +import android.hardware.camera2.marshal.impl.MarshalQueryableRggbChannelVector; +import android.hardware.camera2.marshal.impl.MarshalQueryableSize; +import android.hardware.camera2.marshal.impl.MarshalQueryableSizeF; +import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfiguration; +import android.hardware.camera2.marshal.impl.MarshalQueryableStreamConfigurationDuration; +import android.hardware.camera2.marshal.impl.MarshalQueryableString; +import android.hardware.camera2.params.Face; +import android.hardware.camera2.params.LensShadingMap; +import android.hardware.camera2.params.StreamConfiguration; +import android.hardware.camera2.params.StreamConfigurationDuration; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.hardware.camera2.params.TonemapCurve; +import android.hardware.camera2.utils.TypeReference; +import android.location.Location; +import android.location.LocationManager; import android.os.Parcelable; import android.os.Parcel; import android.util.Log; +import android.util.Pair; +import android.util.Size; -import java.lang.reflect.Array; +import com.android.internal.util.Preconditions; + +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; @@ -38,12 +70,183 @@ import java.util.HashMap; * Implementation of camera metadata marshal/unmarshal across Binder to * the camera service */ -public class CameraMetadataNative extends CameraMetadata implements Parcelable { +public class CameraMetadataNative implements Parcelable { + + public static class Key<T> { + private boolean mHasTag; + private int mTag; + private final Class<T> mType; + private final TypeReference<T> mTypeReference; + private final String mName; + + /** + * Visible for testing only. + * + * <p>Use the CameraCharacteristics.Key, CaptureResult.Key, or CaptureRequest.Key + * for application code or vendor-extended keys.</p> + */ + public Key(String name, Class<T> type) { + if (name == null) { + throw new NullPointerException("Key needs a valid name"); + } else if (type == null) { + throw new NullPointerException("Type needs to be non-null"); + } + mName = name; + mType = type; + mTypeReference = TypeReference.createSpecializedTypeReference(type); + } + + /** + * Visible for testing only. + * + * <p>Use the CameraCharacteristics.Key, CaptureResult.Key, or CaptureRequest.Key + * for application code or vendor-extended keys.</p> + */ + @SuppressWarnings("unchecked") + public Key(String name, TypeReference<T> typeReference) { + if (name == null) { + throw new NullPointerException("Key needs a valid name"); + } else if (typeReference == null) { + throw new NullPointerException("TypeReference needs to be non-null"); + } + mName = name; + mType = (Class<T>)typeReference.getRawType(); + mTypeReference = typeReference; + } + + /** + * Return a camelCase, period separated name formatted like: + * {@code "root.section[.subsections].name"}. + * + * <p>Built-in keys exposed by the Android SDK are always prefixed with {@code "android."}; + * keys that are device/platform-specific are prefixed with {@code "com."}.</p> + * + * <p>For example, {@code CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP} would + * have a name of {@code "android.scaler.streamConfigurationMap"}; whereas a device + * specific key might look like {@code "com.google.nexus.data.private"}.</p> + * + * @return String representation of the key name + */ + public final String getName() { + return mName; + } + + /** + * {@inheritDoc} + */ + @Override + public final int hashCode() { + return mName.hashCode() ^ mTypeReference.hashCode(); + } + + /** + * Compare this key against other native keys, request keys, result keys, and + * characteristics keys. + * + * <p>Two keys are considered equal if their name and type reference are equal.</p> + * + * <p>Note that the equality against non-native keys is one-way. A native key may be equal + * to a result key; but that same result key will not be equal to a native key.</p> + */ + @SuppressWarnings("rawtypes") + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + + Key<?> lhs; + + if (o instanceof CaptureResult.Key) { + lhs = ((CaptureResult.Key)o).getNativeKey(); + } else if (o instanceof CaptureRequest.Key) { + lhs = ((CaptureRequest.Key)o).getNativeKey(); + } else if (o instanceof CameraCharacteristics.Key) { + lhs = ((CameraCharacteristics.Key)o).getNativeKey(); + } else if ((o instanceof Key)) { + lhs = (Key<?>)o; + } else { + return false; + } + + return mName.equals(lhs.mName) && mTypeReference.equals(lhs.mTypeReference); + } + + /** + * <p> + * Get the tag corresponding to this key. This enables insertion into the + * native metadata. + * </p> + * + * <p>This value is looked up the first time, and cached subsequently.</p> + * + * @return The tag numeric value corresponding to the string + */ + public final int getTag() { + if (!mHasTag) { + mTag = CameraMetadataNative.getTag(mName); + mHasTag = true; + } + return mTag; + } + + /** + * Get the raw class backing the type {@code T} for this key. + * + * <p>The distinction is only important if {@code T} is a generic, e.g. + * {@code Range<Integer>} since the nested type will be erased.</p> + */ + public final Class<T> getType() { + // TODO: remove this; other places should use #getTypeReference() instead + return mType; + } + + /** + * Get the type reference backing the type {@code T} for this key. + * + * <p>The distinction is only important if {@code T} is a generic, e.g. + * {@code Range<Integer>} since the nested type will be retained.</p> + */ + public final TypeReference<T> getTypeReference() { + return mTypeReference; + } + } private static final String TAG = "CameraMetadataJV"; private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); // this should be in sync with HAL_PIXEL_FORMAT_BLOB defined in graphics.h - private static final int NATIVE_JPEG_FORMAT = 0x21; + public static final int NATIVE_JPEG_FORMAT = 0x21; + + private static final String CELLID_PROCESS = "CELLID"; + private static final String GPS_PROCESS = "GPS"; + + private static String translateLocationProviderToProcess(final String provider) { + if (provider == null) { + return null; + } + switch(provider) { + case LocationManager.GPS_PROVIDER: + return GPS_PROCESS; + case LocationManager.NETWORK_PROVIDER: + return CELLID_PROCESS; + default: + return null; + } + } + + private static String translateProcessToLocationProvider(final String process) { + if (process == null) { + return null; + } + switch(process) { + case GPS_PROCESS: + return LocationManager.GPS_PROVIDER; + case CELLID_PROCESS: + return LocationManager.NETWORK_PROVIDER; + default: + return null; + } + } public CameraMetadataNative() { super(); @@ -64,6 +267,20 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { } } + /** + * Move the contents from {@code other} into a new camera metadata instance.</p> + * + * <p>After this call, {@code other} will become empty.</p> + * + * @param other the previous metadata instance which will get pilfered + * @return a new metadata instance with the values from {@code other} moved into it + */ + public static CameraMetadataNative move(CameraMetadataNative other) { + CameraMetadataNative newObject = new CameraMetadataNative(); + newObject.swap(other); + return newObject; + } + public static final Parcelable.Creator<CameraMetadataNative> CREATOR = new Parcelable.Creator<CameraMetadataNative>() { @Override @@ -89,12 +306,39 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { nativeWriteToParcel(dest); } - @SuppressWarnings("unchecked") - @Override + /** + * @hide + */ + public <T> T get(CameraCharacteristics.Key<T> key) { + return get(key.getNativeKey()); + } + + /** + * @hide + */ + public <T> T get(CaptureResult.Key<T> key) { + return get(key.getNativeKey()); + } + + /** + * @hide + */ + public <T> T get(CaptureRequest.Key<T> key) { + return get(key.getNativeKey()); + } + + /** + * Look-up a metadata field value by its key. + * + * @param key a non-{@code null} key instance + * @return the field corresponding to the {@code key}, or {@code null} if no value was set + */ public <T> T get(Key<T> key) { - T value = getOverride(key); - if (value != null) { - return value; + Preconditions.checkNotNull(key, "key must not be null"); + + Pair<T, Boolean> override = getOverride(key); + if (override.second) { + return override.first; } return getBase(key); @@ -133,6 +377,18 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { setBase(key, value); } + public <T> void set(CaptureRequest.Key<T> key, T value) { + set(key.getNativeKey(), value); + } + + public <T> void set(CaptureResult.Key<T> key, T value) { + set(key.getNativeKey(), value); + } + + public <T> void set(CameraCharacteristics.Key<T> key, T value) { + set(key.getNativeKey(), value); + } + // Keep up-to-date with camera_metadata.h /** * @hide @@ -169,273 +425,16 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { mMetadataPtr = 0; // set it to 0 again to prevent eclipse from making this field final } - private static int getTypeSize(int nativeType) { - switch(nativeType) { - case TYPE_BYTE: - return 1; - case TYPE_INT32: - case TYPE_FLOAT: - return 4; - case TYPE_INT64: - case TYPE_DOUBLE: - case TYPE_RATIONAL: - return 8; - } - - throw new UnsupportedOperationException("Unknown type, can't get size " - + nativeType); - } - - private static Class<?> getExpectedType(int nativeType) { - switch(nativeType) { - case TYPE_BYTE: - return Byte.TYPE; - case TYPE_INT32: - return Integer.TYPE; - case TYPE_FLOAT: - return Float.TYPE; - case TYPE_INT64: - return Long.TYPE; - case TYPE_DOUBLE: - return Double.TYPE; - case TYPE_RATIONAL: - return Rational.class; - } - - throw new UnsupportedOperationException("Unknown type, can't map to Java type " - + nativeType); - } - - @SuppressWarnings("unchecked") - private static <T> int packSingleNative(T value, ByteBuffer buffer, Class<T> type, - int nativeType, boolean sizeOnly) { - - if (!sizeOnly) { - /** - * Rewrite types when the native type doesn't match the managed type - * - Boolean -> Byte - * - Integer -> Byte - */ - - if (nativeType == TYPE_BYTE && type == Boolean.TYPE) { - // Since a boolean can't be cast to byte, and we don't want to use putBoolean - boolean asBool = (Boolean) value; - byte asByte = (byte) (asBool ? 1 : 0); - value = (T) (Byte) asByte; - } else if (nativeType == TYPE_BYTE && type == Integer.TYPE) { - int asInt = (Integer) value; - byte asByte = (byte) asInt; - value = (T) (Byte) asByte; - } else if (type != getExpectedType(nativeType)) { - throw new UnsupportedOperationException("Tried to pack a type of " + type + - " but we expected the type to be " + getExpectedType(nativeType)); - } - - if (nativeType == TYPE_BYTE) { - buffer.put((Byte) value); - } else if (nativeType == TYPE_INT32) { - buffer.putInt((Integer) value); - } else if (nativeType == TYPE_FLOAT) { - buffer.putFloat((Float) value); - } else if (nativeType == TYPE_INT64) { - buffer.putLong((Long) value); - } else if (nativeType == TYPE_DOUBLE) { - buffer.putDouble((Double) value); - } else if (nativeType == TYPE_RATIONAL) { - Rational r = (Rational) value; - buffer.putInt(r.getNumerator()); - buffer.putInt(r.getDenominator()); - } - - } - - return getTypeSize(nativeType); + private <T> T getBase(CameraCharacteristics.Key<T> key) { + return getBase(key.getNativeKey()); } - @SuppressWarnings({"unchecked", "rawtypes"}) - private static <T> int packSingle(T value, ByteBuffer buffer, Class<T> type, int nativeType, - boolean sizeOnly) { - - int size = 0; - - if (type.isPrimitive() || type == Rational.class) { - size = packSingleNative(value, buffer, type, nativeType, sizeOnly); - } else if (type.isEnum()) { - size = packEnum((Enum)value, buffer, (Class<Enum>)type, nativeType, sizeOnly); - } else if (type.isArray()) { - size = packArray(value, buffer, type, nativeType, sizeOnly); - } else { - size = packClass(value, buffer, type, nativeType, sizeOnly); - } - - return size; - } - - private static <T extends Enum<T>> int packEnum(T value, ByteBuffer buffer, Class<T> type, - int nativeType, boolean sizeOnly) { - - // TODO: add support for enums with their own values. - return packSingleNative(getEnumValue(value), buffer, Integer.TYPE, nativeType, sizeOnly); - } - - @SuppressWarnings("unchecked") - private static <T> int packClass(T value, ByteBuffer buffer, Class<T> type, int nativeType, - boolean sizeOnly) { - - MetadataMarshalClass<T> marshaler = getMarshaler(type, nativeType); - if (marshaler == null) { - throw new IllegalArgumentException(String.format("Unknown Key type: %s", type)); - } - - return marshaler.marshal(value, buffer, nativeType, sizeOnly); - } - - private static <T> int packArray(T value, ByteBuffer buffer, Class<T> type, int nativeType, - boolean sizeOnly) { - - int size = 0; - int arrayLength = Array.getLength(value); - - @SuppressWarnings("unchecked") - Class<Object> componentType = (Class<Object>)type.getComponentType(); - - for (int i = 0; i < arrayLength; ++i) { - size += packSingle(Array.get(value, i), buffer, componentType, nativeType, sizeOnly); - } - - return size; + private <T> T getBase(CaptureResult.Key<T> key) { + return getBase(key.getNativeKey()); } - @SuppressWarnings("unchecked") - private static <T> T unpackSingleNative(ByteBuffer buffer, Class<T> type, int nativeType) { - - T val; - - if (nativeType == TYPE_BYTE) { - val = (T) (Byte) buffer.get(); - } else if (nativeType == TYPE_INT32) { - val = (T) (Integer) buffer.getInt(); - } else if (nativeType == TYPE_FLOAT) { - val = (T) (Float) buffer.getFloat(); - } else if (nativeType == TYPE_INT64) { - val = (T) (Long) buffer.getLong(); - } else if (nativeType == TYPE_DOUBLE) { - val = (T) (Double) buffer.getDouble(); - } else if (nativeType == TYPE_RATIONAL) { - val = (T) new Rational(buffer.getInt(), buffer.getInt()); - } else { - throw new UnsupportedOperationException("Unknown type, can't unpack a native type " - + nativeType); - } - - /** - * Rewrite types when the native type doesn't match the managed type - * - Byte -> Boolean - * - Byte -> Integer - */ - - if (nativeType == TYPE_BYTE && type == Boolean.TYPE) { - // Since a boolean can't be cast to byte, and we don't want to use getBoolean - byte asByte = (Byte) val; - boolean asBool = asByte != 0; - val = (T) (Boolean) asBool; - } else if (nativeType == TYPE_BYTE && type == Integer.TYPE) { - byte asByte = (Byte) val; - int asInt = asByte; - val = (T) (Integer) asInt; - } else if (type != getExpectedType(nativeType)) { - throw new UnsupportedOperationException("Tried to unpack a type of " + type + - " but we expected the type to be " + getExpectedType(nativeType)); - } - - return val; - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - private static <T> T unpackSingle(ByteBuffer buffer, Class<T> type, int nativeType) { - - if (type.isPrimitive() || type == Rational.class) { - return unpackSingleNative(buffer, type, nativeType); - } - - if (type.isEnum()) { - return (T) unpackEnum(buffer, (Class<Enum>)type, nativeType); - } - - if (type.isArray()) { - return unpackArray(buffer, type, nativeType); - } - - T instance = unpackClass(buffer, type, nativeType); - - return instance; - } - - private static <T extends Enum<T>> T unpackEnum(ByteBuffer buffer, Class<T> type, - int nativeType) { - int ordinal = unpackSingleNative(buffer, Integer.TYPE, nativeType); - return getEnumFromValue(type, ordinal); - } - - private static <T> T unpackClass(ByteBuffer buffer, Class<T> type, int nativeType) { - - MetadataMarshalClass<T> marshaler = getMarshaler(type, nativeType); - if (marshaler == null) { - throw new IllegalArgumentException("Unknown class type: " + type); - } - - return marshaler.unmarshal(buffer, nativeType); - } - - @SuppressWarnings("unchecked") - private static <T> T unpackArray(ByteBuffer buffer, Class<T> type, int nativeType) { - - Class<?> componentType = type.getComponentType(); - Object array; - - int elementSize = getTypeSize(nativeType); - - MetadataMarshalClass<?> marshaler = getMarshaler(componentType, nativeType); - if (marshaler != null) { - elementSize = marshaler.getNativeSize(nativeType); - } - - if (elementSize != MetadataMarshalClass.NATIVE_SIZE_DYNAMIC) { - int remaining = buffer.remaining(); - int arraySize = remaining / elementSize; - - if (VERBOSE) { - Log.v(TAG, - String.format( - "Attempting to unpack array (count = %d, element size = %d, bytes " + - "remaining = %d) for type %s", - arraySize, elementSize, remaining, type)); - } - - array = Array.newInstance(componentType, arraySize); - for (int i = 0; i < arraySize; ++i) { - Object elem = unpackSingle(buffer, componentType, nativeType); - Array.set(array, i, elem); - } - } else { - // Dynamic size, use an array list. - ArrayList<Object> arrayList = new ArrayList<Object>(); - - int primitiveSize = getTypeSize(nativeType); - while (buffer.remaining() >= primitiveSize) { - Object elem = unpackSingle(buffer, componentType, nativeType); - arrayList.add(elem); - } - - array = arrayList.toArray((T[]) Array.newInstance(componentType, 0)); - } - - if (buffer.remaining() != 0) { - Log.e(TAG, "Trailing bytes (" + buffer.remaining() + ") left over after unpacking " - + type); - } - - return (T) array; + private <T> T getBase(CaptureRequest.Key<T> key) { + return getBase(key.getNativeKey()); } private <T> T getBase(Key<T> key) { @@ -445,30 +444,48 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return null; } - int nativeType = getNativeType(tag); - + Marshaler<T> marshaler = getMarshalerForKey(key); ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder()); - return unpackSingle(buffer, key.getType(), nativeType); + return marshaler.unmarshal(buffer); } - // Need overwrite some metadata that has different definitions between native // and managed sides. @SuppressWarnings("unchecked") - private <T> T getOverride(Key<T> key) { + private <T> Pair<T, Boolean> getOverride(Key<T> key) { + T value = null; + boolean override = true; + if (key.equals(CameraCharacteristics.SCALER_AVAILABLE_FORMATS)) { - return (T) getAvailableFormats(); + value = (T) getAvailableFormats(); } else if (key.equals(CaptureResult.STATISTICS_FACES)) { - return (T) getFaces(); + value = (T) getFaces(); } else if (key.equals(CaptureResult.STATISTICS_FACE_RECTANGLES)) { - 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(); + value = (T) getFaceRectangles(); + } else if (key.equals(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)) { + value = (T) getStreamConfigurationMap(); + } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)) { + value = (T) getMaxRegions(key); + } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB)) { + value = (T) getMaxRegions(key); + } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)) { + value = (T) getMaxRegions(key); + } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_RAW)) { + value = (T) getMaxNumOutputs(key); + } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC)) { + value = (T) getMaxNumOutputs(key); + } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC_STALLING)) { + value = (T) getMaxNumOutputs(key); + } else if (key.equals(CaptureRequest.TONEMAP_CURVE)) { + value = (T) getTonemapCurve(); + } else if (key.equals(CaptureResult.JPEG_GPS_LOCATION)) { + value = (T) getGpsLocation(); + } else if (key.equals(CaptureResult.STATISTICS_LENS_SHADING_CORRECTION_MAP)) { + value = (T) getLensShadingMap(); + } else { + override = false; } - // For other keys, get() falls back to getBase() - return null; + return Pair.create(value, override); } private int[] getAvailableFormats() { @@ -485,50 +502,6 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { 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; @@ -628,23 +601,159 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return fixedFaceRectangles; } + private LensShadingMap getLensShadingMap() { + float[] lsmArray = getBase(CaptureResult.STATISTICS_LENS_SHADING_MAP); + if (lsmArray == null) { + Log.w(TAG, "getLensShadingMap - Lens shading map was null."); + return null; + } + Size s = get(CameraCharacteristics.LENS_INFO_SHADING_MAP_SIZE); + LensShadingMap map = new LensShadingMap(lsmArray, s.getHeight(), s.getWidth()); + return map; + } + + private Location getGpsLocation() { + String processingMethod = get(CaptureResult.JPEG_GPS_PROCESSING_METHOD); + Location l = new Location(translateProcessToLocationProvider(processingMethod)); + + double[] coords = get(CaptureResult.JPEG_GPS_COORDINATES); + Long timeStamp = get(CaptureResult.JPEG_GPS_TIMESTAMP); + + if (timeStamp != null) { + l.setTime(timeStamp); + } else { + Log.w(TAG, "getGpsLocation - No timestamp for GPS location."); + } + + if (coords != null) { + l.setLatitude(coords[0]); + l.setLongitude(coords[1]); + l.setAltitude(coords[2]); + } else { + Log.w(TAG, "getGpsLocation - No coordinates for GPS location"); + } + + return l; + } + + private boolean setGpsLocation(Location l) { + if (l == null) { + return false; + } + + double[] coords = { l.getLatitude(), l.getLongitude(), l.getAltitude() }; + String processMethod = translateLocationProviderToProcess(l.getProvider()); + long timestamp = l.getTime(); + + set(CaptureRequest.JPEG_GPS_TIMESTAMP, timestamp); + set(CaptureRequest.JPEG_GPS_COORDINATES, coords); + + if (processMethod == null) { + Log.w(TAG, "setGpsLocation - No process method, Location is not from a GPS or NETWORK" + + "provider"); + } else { + setBase(CaptureRequest.JPEG_GPS_PROCESSING_METHOD, processMethod); + } + return true; + } + + private StreamConfigurationMap getStreamConfigurationMap() { + StreamConfiguration[] configurations = getBase( + CameraCharacteristics.SCALER_AVAILABLE_STREAM_CONFIGURATIONS); + StreamConfigurationDuration[] minFrameDurations = getBase( + CameraCharacteristics.SCALER_AVAILABLE_MIN_FRAME_DURATIONS); + StreamConfigurationDuration[] stallDurations = getBase( + CameraCharacteristics.SCALER_AVAILABLE_STALL_DURATIONS); + + return new StreamConfigurationMap(configurations, minFrameDurations, stallDurations); + } + + private <T> Integer getMaxRegions(Key<T> key) { + final int AE = 0; + final int AWB = 1; + final int AF = 2; + + // The order of the elements is: (AE, AWB, AF) + int[] maxRegions = getBase(CameraCharacteristics.CONTROL_MAX_REGIONS); + + if (maxRegions == null) { + return null; + } + + if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)) { + return maxRegions[AE]; + } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AWB)) { + return maxRegions[AWB]; + } else if (key.equals(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)) { + return maxRegions[AF]; + } else { + throw new AssertionError("Invalid key " + key); + } + } + + private <T> Integer getMaxNumOutputs(Key<T> key) { + final int RAW = 0; + final int PROC = 1; + final int PROC_STALLING = 2; + + // The order of the elements is: (raw, proc+nonstalling, proc+stalling) + int[] maxNumOutputs = getBase(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_STREAMS); + + if (maxNumOutputs == null) { + return null; + } + + if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_RAW)) { + return maxNumOutputs[RAW]; + } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC)) { + return maxNumOutputs[PROC]; + } else if (key.equals(CameraCharacteristics.REQUEST_MAX_NUM_OUTPUT_PROC_STALLING)) { + return maxNumOutputs[PROC_STALLING]; + } else { + throw new AssertionError("Invalid key " + key); + } + } + + private <T> TonemapCurve getTonemapCurve() { + float[] red = getBase(CaptureRequest.TONEMAP_CURVE_RED); + float[] green = getBase(CaptureRequest.TONEMAP_CURVE_GREEN); + float[] blue = getBase(CaptureRequest.TONEMAP_CURVE_BLUE); + if (red == null || green == null || blue == null) { + return null; + } + TonemapCurve tc = new TonemapCurve(red, green, blue); + return tc; + } + + private <T> void setBase(CameraCharacteristics.Key<T> key, T value) { + setBase(key.getNativeKey(), value); + } + + private <T> void setBase(CaptureResult.Key<T> key, T value) { + setBase(key.getNativeKey(), value); + } + + private <T> void setBase(CaptureRequest.Key<T> key, T value) { + setBase(key.getNativeKey(), value); + } + private <T> void setBase(Key<T> key, T value) { int tag = key.getTag(); if (value == null) { - writeValues(tag, null); + // Erase the entry + writeValues(tag, /*src*/null); return; - } - - int nativeType = getNativeType(tag); + } // else update the entry to a new value - int size = packSingle(value, null, key.getType(), nativeType, /* sizeOnly */true); + Marshaler<T> marshaler = getMarshalerForKey(key); + int size = marshaler.calculateMarshalSize(value); // TODO: Optimization. Cache the byte[] and reuse if the size is big enough. byte[] values = new byte[size]; ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder()); - packSingle(value, buffer, key.getType(), nativeType, /*sizeOnly*/false); + marshaler.marshal(value, buffer); writeValues(tag, values); } @@ -655,56 +764,15 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { 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); + } else if (key.equals(CaptureRequest.TONEMAP_CURVE)) { + return setTonemapCurve((TonemapCurve) value); + } else if (key.equals(CaptureResult.JPEG_GPS_LOCATION)) { + return setGpsLocation((Location) 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) { @@ -754,6 +822,24 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return true; } + private <T> boolean setTonemapCurve(TonemapCurve tc) { + if (tc == null) { + return false; + } + + float[][] curve = new float[3][]; + for (int i = TonemapCurve.CHANNEL_RED; i <= TonemapCurve.CHANNEL_BLUE; i++) { + int pointCount = tc.getPointCount(i); + curve[i] = new float[pointCount * TonemapCurve.POINT_SIZE]; + tc.copyColorCurve(i, curve[i], 0); + } + setBase(CaptureRequest.TONEMAP_CURVE_RED, curve[0]); + setBase(CaptureRequest.TONEMAP_CURVE_GREEN, curve[1]); + setBase(CaptureRequest.TONEMAP_CURVE_BLUE, curve[2]); + + return true; + } + private long mMetadataPtr; // native CameraMetadata* private native long nativeAllocate(); @@ -770,6 +856,7 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { private native synchronized byte[] nativeReadValues(int tag); private native synchronized void nativeWriteValues(int tag, byte[] src); + private native synchronized void nativeDump() throws IOException; // dump to ALOGD private static native int nativeGetTagFromKey(String keyName) throws IllegalArgumentException; @@ -861,134 +948,89 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return nativeReadValues(tag); } - @Override - protected void finalize() throws Throwable { - try { - close(); - } finally { - super.finalize(); - } - } - - private static final HashMap<Class<? extends Enum>, int[]> sEnumValues = - new HashMap<Class<? extends Enum>, int[]>(); /** - * Register a non-sequential set of values to be used with the pack/unpack functions. - * This enables get/set to correctly marshal the enum into a value that is C-compatible. + * Dumps the native metadata contents to logcat. * - * @param enumType The class for an enum - * @param values A list of values mapping to the ordinals of the enum + * <p>Visibility for testing/debugging only. The results will not + * include any synthesized keys, as they are invisible to the native layer.</p> * * @hide */ - public static <T extends Enum<T>> void registerEnumValues(Class<T> enumType, int[] values) { - if (enumType.getEnumConstants().length != values.length) { - throw new IllegalArgumentException( - "Expected values array to be the same size as the enumTypes values " - + values.length + " for type " + enumType); - } - if (VERBOSE) { - Log.v(TAG, "Registered enum values for type " + enumType + " values"); + public void dumpToLog() { + try { + nativeDump(); + } catch (IOException e) { + Log.wtf(TAG, "Dump logging failed", e); } - - sEnumValues.put(enumType, values); } - /** - * Get the numeric value from an enum. This is usually the same as the ordinal value for - * enums that have fully sequential values, although for C-style enums the range of values - * may not map 1:1. - * - * @param enumValue Enum instance - * @return Int guaranteed to be ABI-compatible with the C enum equivalent - */ - private static <T extends Enum<T>> int getEnumValue(T enumValue) { - int[] values; - values = sEnumValues.get(enumValue.getClass()); - - int ordinal = enumValue.ordinal(); - if (values != null) { - return values[ordinal]; + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); } - - return ordinal; } /** - * Finds the enum corresponding to it's numeric value. Opposite of {@link #getEnumValue} method. + * Get the marshaler compatible with the {@code key} and type {@code T}. * - * @param enumType Class of the enum we want to find - * @param value The numeric value of the enum - * @return An instance of the enum + * @throws UnsupportedOperationException + * if the native/managed type combination for {@code key} is not supported */ - private static <T extends Enum<T>> T getEnumFromValue(Class<T> enumType, int value) { - int ordinal; - - int[] registeredValues = sEnumValues.get(enumType); - if (registeredValues != null) { - ordinal = -1; - - for (int i = 0; i < registeredValues.length; ++i) { - if (registeredValues[i] == value) { - ordinal = i; - break; - } - } - } else { - ordinal = value; - } - - T[] values = enumType.getEnumConstants(); - - if (ordinal < 0 || ordinal >= values.length) { - throw new IllegalArgumentException( - String.format( - "Argument 'value' (%d) was not a valid enum value for type %s " - + "(registered? %b)", - value, - enumType, (registeredValues != null))); - } - - return values[ordinal]; - } - - static HashMap<Class<?>, MetadataMarshalClass<?>> sMarshalerMap = new - HashMap<Class<?>, MetadataMarshalClass<?>>(); - - private static <T> void registerMarshaler(MetadataMarshalClass<T> marshaler) { - sMarshalerMap.put(marshaler.getMarshalingClass(), marshaler); + private static <T> Marshaler<T> getMarshalerForKey(Key<T> key) { + return MarshalRegistry.getMarshaler(key.getTypeReference(), + getNativeType(key.getTag())); } - @SuppressWarnings("unchecked") - private static <T> MetadataMarshalClass<T> getMarshaler(Class<T> type, int nativeType) { - MetadataMarshalClass<T> marshaler = (MetadataMarshalClass<T>) sMarshalerMap.get(type); - - if (marshaler != null && !marshaler.isNativeTypeSupported(nativeType)) { - throw new UnsupportedOperationException("Unsupported type " + nativeType + - " to be marshalled to/from a " + type); - } - - return marshaler; - } - - /** - * We use a class initializer to allow the native code to cache some field offsets - */ - static { - nativeClassInit(); - + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static void registerAllMarshalers() { if (VERBOSE) { Log.v(TAG, "Shall register metadata marshalers"); } - // load built-in marshallers - registerMarshaler(new MetadataMarshalRect()); - registerMarshaler(new MetadataMarshalSize()); - registerMarshaler(new MetadataMarshalString()); - + MarshalQueryable[] queryList = new MarshalQueryable[] { + // marshalers for standard types + new MarshalQueryablePrimitive(), + new MarshalQueryableEnum(), + new MarshalQueryableArray(), + + // pseudo standard types, that expand/narrow the native type into a managed type + new MarshalQueryableBoolean(), + new MarshalQueryableNativeByteToInteger(), + + // marshalers for custom types + new MarshalQueryableRect(), + new MarshalQueryableSize(), + new MarshalQueryableSizeF(), + new MarshalQueryableString(), + new MarshalQueryableReprocessFormatsMap(), + new MarshalQueryableRange(), + new MarshalQueryablePair(), + new MarshalQueryableMeteringRectangle(), + new MarshalQueryableColorSpaceTransform(), + new MarshalQueryableStreamConfiguration(), + new MarshalQueryableStreamConfigurationDuration(), + new MarshalQueryableRggbChannelVector(), + + // generic parcelable marshaler (MUST BE LAST since it has lowest priority) + new MarshalQueryableParcelable(), + }; + + for (MarshalQueryable query : queryList) { + MarshalRegistry.registerMarshalQueryable(query); + } if (VERBOSE) { Log.v(TAG, "Registered metadata marshalers"); } } + static { + /* + * We use a class initializer to allow the native code to cache some field offsets + */ + nativeClassInit(); + registerAllMarshalers(); + } } diff --git a/core/java/android/hardware/camera2/CaptureResultExtras.aidl b/core/java/android/hardware/camera2/impl/CaptureResultExtras.aidl index 6587f02..ebc812a 100644 --- a/core/java/android/hardware/camera2/CaptureResultExtras.aidl +++ b/core/java/android/hardware/camera2/impl/CaptureResultExtras.aidl @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.impl; /** @hide */ parcelable CaptureResultExtras; diff --git a/core/java/android/hardware/camera2/CaptureResultExtras.java b/core/java/android/hardware/camera2/impl/CaptureResultExtras.java index e5c2c1c..7544045 100644 --- a/core/java/android/hardware/camera2/CaptureResultExtras.java +++ b/core/java/android/hardware/camera2/impl/CaptureResultExtras.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.impl; import android.os.Parcel; import android.os.Parcelable; @@ -45,6 +45,15 @@ public class CaptureResultExtras implements Parcelable { readFromParcel(in); } + public CaptureResultExtras(int requestId, int subsequenceId, int afTriggerId, + int precaptureTriggerId, long frameNumber) { + this.requestId = requestId; + this.subsequenceId = subsequenceId; + this.afTriggerId = afTriggerId; + this.precaptureTriggerId = precaptureTriggerId; + this.frameNumber = frameNumber; + } + @Override public int describeContents() { return 0; diff --git a/core/java/android/hardware/camera2/impl/MetadataMarshalClass.java b/core/java/android/hardware/camera2/impl/MetadataMarshalClass.java deleted file mode 100644 index 6d224ef..0000000 --- a/core/java/android/hardware/camera2/impl/MetadataMarshalClass.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.hardware.camera2.impl; - -import java.nio.ByteBuffer; - -public interface MetadataMarshalClass<T> { - - /** - * Marshal the specified object instance (value) into a byte buffer. - * - * @param value the value of type T that we wish to write into the byte buffer - * @param buffer the byte buffer into which the marshalled object will be written - * @param nativeType the native type, e.g. - * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE}. - * Guaranteed to be one for which isNativeTypeSupported returns true. - * @param sizeOnly if this is true, don't write to the byte buffer. calculate the size only. - * @return the size that needs to be written to the byte buffer - */ - int marshal(T value, ByteBuffer buffer, int nativeType, boolean sizeOnly); - - /** - * Unmarshal a new object instance from the byte buffer. - * @param buffer the byte buffer, from which we will read the object - * @param nativeType the native type, e.g. - * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE}. - * Guaranteed to be one for which isNativeTypeSupported returns true. - * @return a new instance of type T read from the byte buffer - */ - T unmarshal(ByteBuffer buffer, int nativeType); - - Class<T> getMarshalingClass(); - - /** - * Determines whether or not this marshaller supports this native type. Most marshallers - * will are likely to only support one type. - * - * @param nativeType the native type, e.g. - * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE} - * @return true if it supports, false otherwise - */ - boolean isNativeTypeSupported(int nativeType); - - public static int NATIVE_SIZE_DYNAMIC = -1; - - /** - * How many bytes T will take up if marshalled to/from nativeType - * @param nativeType the native type, e.g. - * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE} - * @return a size in bytes, or NATIVE_SIZE_DYNAMIC if the size is dynamic - */ - int getNativeSize(int nativeType); -} diff --git a/core/java/android/hardware/camera2/impl/MetadataMarshalRect.java b/core/java/android/hardware/camera2/impl/MetadataMarshalRect.java deleted file mode 100644 index ab72c4f..0000000 --- a/core/java/android/hardware/camera2/impl/MetadataMarshalRect.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.hardware.camera2.impl; - -import android.graphics.Rect; - -import java.nio.ByteBuffer; - -public class MetadataMarshalRect implements MetadataMarshalClass<Rect> { - private static final int SIZE = 16; - - @Override - public int marshal(Rect value, ByteBuffer buffer, int nativeType, boolean sizeOnly) { - if (sizeOnly) { - return SIZE; - } - - buffer.putInt(value.left); - buffer.putInt(value.top); - buffer.putInt(value.width()); - buffer.putInt(value.height()); - - return SIZE; - } - - @Override - public Rect unmarshal(ByteBuffer buffer, int nativeType) { - - int left = buffer.getInt(); - int top = buffer.getInt(); - int width = buffer.getInt(); - int height = buffer.getInt(); - - int right = left + width; - int bottom = top + height; - - return new Rect(left, top, right, bottom); - } - - @Override - public Class<Rect> getMarshalingClass() { - return Rect.class; - } - - @Override - public boolean isNativeTypeSupported(int nativeType) { - return nativeType == CameraMetadataNative.TYPE_INT32; - } - - @Override - public int getNativeSize(int nativeType) { - return SIZE; - } -} diff --git a/core/java/android/hardware/camera2/impl/MetadataMarshalSize.java b/core/java/android/hardware/camera2/impl/MetadataMarshalSize.java deleted file mode 100644 index e8143e0..0000000 --- a/core/java/android/hardware/camera2/impl/MetadataMarshalSize.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.hardware.camera2.impl; - -import android.hardware.camera2.Size; - -import java.nio.ByteBuffer; - -public class MetadataMarshalSize implements MetadataMarshalClass<Size> { - - private static final int SIZE = 8; - - @Override - public int marshal(Size value, ByteBuffer buffer, int nativeType, boolean sizeOnly) { - if (sizeOnly) { - return SIZE; - } - - buffer.putInt(value.getWidth()); - buffer.putInt(value.getHeight()); - - return SIZE; - } - - @Override - public Size unmarshal(ByteBuffer buffer, int nativeType) { - int width = buffer.getInt(); - int height = buffer.getInt(); - - return new Size(width, height); - } - - @Override - public Class<Size> getMarshalingClass() { - return Size.class; - } - - @Override - public boolean isNativeTypeSupported(int nativeType) { - return nativeType == CameraMetadataNative.TYPE_INT32; - } - - @Override - public int getNativeSize(int nativeType) { - return SIZE; - } -} diff --git a/core/java/android/hardware/camera2/impl/MetadataMarshalString.java b/core/java/android/hardware/camera2/impl/MetadataMarshalString.java deleted file mode 100644 index b61b8d3..0000000 --- a/core/java/android/hardware/camera2/impl/MetadataMarshalString.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * 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.hardware.camera2.impl; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; - -public class MetadataMarshalString implements MetadataMarshalClass<String> { - - private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); - - @Override - public int marshal(String value, ByteBuffer buffer, int nativeType, boolean sizeOnly) { - byte[] arr = value.getBytes(UTF8_CHARSET); - - if (!sizeOnly) { - buffer.put(arr); - buffer.put((byte)0); // metadata strings are NULL-terminated - } - - return arr.length + 1; - } - - @Override - public String unmarshal(ByteBuffer buffer, int nativeType) { - - buffer.mark(); // save the current position - - boolean foundNull = false; - int stringLength = 0; - while (buffer.hasRemaining()) { - if (buffer.get() == (byte)0) { - foundNull = true; - break; - } - - stringLength++; - } - if (!foundNull) { - throw new IllegalArgumentException("Strings must be null-terminated"); - } - - buffer.reset(); // go back to the previously marked position - - byte[] strBytes = new byte[stringLength + 1]; - buffer.get(strBytes, /*dstOffset*/0, stringLength + 1); // including null character - - // not including null character - return new String(strBytes, /*offset*/0, stringLength, UTF8_CHARSET); - } - - @Override - public Class<String> getMarshalingClass() { - return String.class; - } - - @Override - public boolean isNativeTypeSupported(int nativeType) { - return nativeType == CameraMetadataNative.TYPE_BYTE; - } - - @Override - public int getNativeSize(int nativeType) { - return NATIVE_SIZE_DYNAMIC; - } -} diff --git a/core/java/android/hardware/camera2/legacy/BurstHolder.java b/core/java/android/hardware/camera2/legacy/BurstHolder.java new file mode 100644 index 0000000..e35eb50 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/BurstHolder.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.hardware.camera2.legacy; + +import android.hardware.camera2.CaptureRequest; + +import java.util.ArrayList; +import java.util.List; + +/** + * Immutable container for a burst of capture results. + */ +public class BurstHolder { + + private final ArrayList<CaptureRequest> mRequests; + private final boolean mRepeating; + private final int mRequestId; + + /** + * Immutable container for a burst of capture results. + * + * @param requestId id of the burst request. + * @param repeating true if this burst is repeating. + * @param requests a {@link java.util.List} of {@link CaptureRequest}s in this burst. + */ + public BurstHolder(int requestId, boolean repeating, List<CaptureRequest> requests) { + mRequests = new ArrayList<CaptureRequest>(requests); + mRepeating = repeating; + mRequestId = requestId; + } + + /** + * Get the id of this request. + */ + public int getRequestId() { + return mRequestId; + } + + /** + * Return true if this repeating. + */ + public boolean isRepeating() { + return mRepeating; + } + + /** + * Return the number of requests in this burst sequence. + */ + public int getNumberOfRequests() { + return mRequests.size(); + } + + /** + * Create a list of {@link RequestHolder} objects encapsulating the requests in this burst. + * + * @param frameNumber the starting framenumber for this burst. + * @return the list of {@link RequestHolder} objects. + */ + public List<RequestHolder> produceRequestHolders(long frameNumber) { + ArrayList<RequestHolder> holders = new ArrayList<RequestHolder>(); + int i = 0; + for (CaptureRequest r : mRequests) { + holders.add(new RequestHolder(mRequestId, i, r, mRepeating, frameNumber + i)); + ++i; + } + return holders; + } +} diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceState.java b/core/java/android/hardware/camera2/legacy/CameraDeviceState.java new file mode 100644 index 0000000..22ff9c6 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceState.java @@ -0,0 +1,259 @@ +/* + * 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.legacy; + +import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.CameraBinderDecorator; +import android.os.Handler; +import android.util.Log; + +/** + * Emulates a the state of a single Camera2 device. + * + * <p> + * This class acts as the state machine for a camera device. Valid state transitions are given + * in the table below: + * </p> + * + * <ul> + * <li>{@code UNCONFIGURED -> CONFIGURING}</li> + * <li>{@code CONFIGURING -> IDLE}</li> + * <li>{@code IDLE -> CONFIGURING}</li> + * <li>{@code IDLE -> CAPTURING}</li> + * <li>{@code CAPTURING -> IDLE}</li> + * <li>{@code ANY -> ERROR}</li> + * </ul> + */ +public class CameraDeviceState { + private static final String TAG = "CameraDeviceState"; + private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + + private static final int STATE_ERROR = 0; + private static final int STATE_UNCONFIGURED = 1; + private static final int STATE_CONFIGURING = 2; + private static final int STATE_IDLE = 3; + private static final int STATE_CAPTURING = 4; + + private int mCurrentState = STATE_UNCONFIGURED; + private int mCurrentError = CameraBinderDecorator.NO_ERROR; + + private RequestHolder mCurrentRequest = null; + + private Handler mCurrentHandler = null; + private CameraDeviceStateListener mCurrentListener = null; + + + /** + * CameraDeviceStateListener callbacks to be called after state transitions. + */ + public interface CameraDeviceStateListener { + void onError(int errorCode, RequestHolder holder); + void onConfiguring(); + void onIdle(); + void onCaptureStarted(RequestHolder holder); + void onCaptureResult(CameraMetadataNative result, RequestHolder holder); + } + + /** + * Transition to the {@code ERROR} state. + * + * <p> + * The device cannot exit the {@code ERROR} state. If the device was not already in the + * {@code ERROR} state, {@link CameraDeviceStateListener#onError(int, RequestHolder)} will be + * called. + * </p> + * + * @param error the error to set. Should be one of the error codes defined in + * {@link android.hardware.camera2.utils.CameraBinderDecorator}. + */ + public synchronized void setError(int error) { + mCurrentError = error; + doStateTransition(STATE_ERROR); + } + + /** + * Transition to the {@code CONFIGURING} state, or {@code ERROR} if in an invalid state. + * + * <p> + * If the device was not already in the {@code CONFIGURING} state, + * {@link CameraDeviceStateListener#onConfiguring()} will be called. + * </p> + * + * @return {@link CameraBinderDecorator#NO_ERROR}, or an error if one has occurred. + */ + public synchronized int setConfiguring() { + doStateTransition(STATE_CONFIGURING); + return mCurrentError; + } + + /** + * Transition to the {@code IDLE} state, or {@code ERROR} if in an invalid state. + * + * <p> + * If the device was not already in the {@code IDLE} state, + * {@link CameraDeviceStateListener#onIdle()} will be called. + * </p> + * + * @return {@link CameraBinderDecorator#NO_ERROR}, or an error if one has occurred. + */ + public synchronized int setIdle() { + doStateTransition(STATE_IDLE); + return mCurrentError; + } + + /** + * Transition to the {@code CAPTURING} state, or {@code ERROR} if in an invalid state. + * + * <p> + * If the device was not already in the {@code CAPTURING} state, + * {@link CameraDeviceStateListener#onCaptureStarted(RequestHolder)} will be called. + * </p> + * + * @param request A {@link RequestHolder} containing the request for the current capture. + * @return {@link CameraBinderDecorator#NO_ERROR}, or an error if one has occurred. + */ + public synchronized int setCaptureStart(final RequestHolder request) { + mCurrentRequest = request; + doStateTransition(STATE_CAPTURING); + return mCurrentError; + } + + /** + * Set the result for a capture. + * + * <p> + * If the device was in the {@code CAPTURING} state, + * {@link CameraDeviceStateListener#onCaptureResult(CameraMetadataNative, RequestHolder)} will + * be called with the given result, otherwise this will result in the device transitioning to + * the {@code ERROR} state, + * </p> + * + * @param request the {@link RequestHolder} request that created this result. + * @param result the {@link CameraMetadataNative} result to set. + * @return {@link CameraBinderDecorator#NO_ERROR}, or an error if one has occurred. + */ + public synchronized int setCaptureResult(final RequestHolder request, + final CameraMetadataNative result) { + if (mCurrentState != STATE_CAPTURING) { + Log.e(TAG, "Cannot receive result while in state: " + mCurrentState); + mCurrentError = CameraBinderDecorator.INVALID_OPERATION; + doStateTransition(STATE_ERROR); + return mCurrentError; + } + + if (mCurrentHandler != null && mCurrentListener != null) { + mCurrentHandler.post(new Runnable() { + @Override + public void run() { + mCurrentListener.onCaptureResult(result, request); + } + }); + } + return mCurrentError; + } + + /** + * Set the listener for state transition callbacks. + * + * @param handler handler on which to call the callbacks. + * @param listener the {@link CameraDeviceStateListener} callbacks to call. + */ + public synchronized void setCameraDeviceCallbacks(Handler handler, + CameraDeviceStateListener listener) { + mCurrentHandler = handler; + mCurrentListener = listener; + } + + private void doStateTransition(int newState) { + if (DEBUG) { + if (newState != mCurrentState) { + Log.d(TAG, "Transitioning to state " + newState); + } + } + switch(newState) { + case STATE_ERROR: + if (mCurrentState != STATE_ERROR && mCurrentHandler != null && + mCurrentListener != null) { + mCurrentHandler.post(new Runnable() { + @Override + public void run() { + mCurrentListener.onError(mCurrentError, mCurrentRequest); + } + }); + } + mCurrentState = STATE_ERROR; + break; + case STATE_CONFIGURING: + if (mCurrentState != STATE_UNCONFIGURED && mCurrentState != STATE_IDLE) { + Log.e(TAG, "Cannot call configure while in state: " + mCurrentState); + mCurrentError = CameraBinderDecorator.INVALID_OPERATION; + doStateTransition(STATE_ERROR); + break; + } + if (mCurrentState != STATE_CONFIGURING && mCurrentHandler != null && + mCurrentListener != null) { + mCurrentHandler.post(new Runnable() { + @Override + public void run() { + mCurrentListener.onConfiguring(); + } + }); + } + mCurrentState = STATE_CONFIGURING; + break; + case STATE_IDLE: + if (mCurrentState != STATE_CONFIGURING && mCurrentState != STATE_CAPTURING) { + Log.e(TAG, "Cannot call idle while in state: " + mCurrentState); + mCurrentError = CameraBinderDecorator.INVALID_OPERATION; + doStateTransition(STATE_ERROR); + break; + } + if (mCurrentState != STATE_IDLE && mCurrentHandler != null && + mCurrentListener != null) { + mCurrentHandler.post(new Runnable() { + @Override + public void run() { + mCurrentListener.onIdle(); + } + }); + } + mCurrentState = STATE_IDLE; + break; + case STATE_CAPTURING: + if (mCurrentState != STATE_IDLE && mCurrentState != STATE_CAPTURING) { + Log.e(TAG, "Cannot call capture while in state: " + mCurrentState); + mCurrentError = CameraBinderDecorator.INVALID_OPERATION; + doStateTransition(STATE_ERROR); + break; + } + if (mCurrentHandler != null && mCurrentListener != null) { + mCurrentHandler.post(new Runnable() { + @Override + public void run() { + mCurrentListener.onCaptureStarted(mCurrentRequest); + } + }); + } + mCurrentState = STATE_CAPTURING; + break; + default: + throw new IllegalStateException("Transition to unknown state: " + newState); + } + } + + +} diff --git a/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java new file mode 100644 index 0000000..54d9c3c --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/CameraDeviceUserShim.java @@ -0,0 +1,273 @@ +/* + * 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.legacy; + +import android.hardware.Camera; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.ICameraDeviceCallbacks; +import android.hardware.camera2.ICameraDeviceUser; +import android.hardware.camera2.utils.LongParcelable; +import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.CameraBinderDecorator; +import android.hardware.camera2.utils.CameraRuntimeException; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.Log; +import android.util.SparseArray; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Compatibility implementation of the Camera2 API binder interface. + * + * <p> + * This is intended to be called from the same process as client + * {@link android.hardware.camera2.CameraDevice}, and wraps a + * {@link android.hardware.camera2.legacy.LegacyCameraDevice} that emulates Camera2 service using + * the Camera1 API. + * </p> + * + * <p> + * Keep up to date with ICameraDeviceUser.aidl. + * </p> + */ +public class CameraDeviceUserShim implements ICameraDeviceUser { + private static final String TAG = "CameraDeviceUserShim"; + + private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + + private final LegacyCameraDevice mLegacyDevice; + + private final Object mConfigureLock = new Object(); + private int mSurfaceIdCounter; + private boolean mConfiguring; + private final SparseArray<Surface> mSurfaces; + + protected CameraDeviceUserShim(int cameraId, LegacyCameraDevice legacyCamera) { + mLegacyDevice = legacyCamera; + mConfiguring = false; + mSurfaces = new SparseArray<Surface>(); + + mSurfaceIdCounter = 0; + } + + public static CameraDeviceUserShim connectBinderShim(ICameraDeviceCallbacks callbacks, + int cameraId) { + if (DEBUG) { + Log.d(TAG, "Opening shim Camera device"); + } + // TODO: Move open/init into LegacyCameraDevice thread when API is switched to async. + Camera legacyCamera = Camera.openUninitialized(); + int initErrors = legacyCamera.cameraInit(cameraId); + // Check errors old HAL initialization + if (Camera.checkInitErrors(initErrors)) { + // TODO: Map over old camera error codes. This likely involves improving the error + // reporting in the HAL1 connect path. + throw new CameraRuntimeException(CameraAccessException.CAMERA_DISCONNECTED); + } + LegacyCameraDevice device = new LegacyCameraDevice(cameraId, legacyCamera, callbacks); + return new CameraDeviceUserShim(cameraId, device); + } + + @Override + public void disconnect() { + if (DEBUG) { + Log.d(TAG, "disconnect called."); + } + mLegacyDevice.close(); + } + + @Override + public int submitRequest(CaptureRequest request, boolean streaming, + /*out*/LongParcelable lastFrameNumber) { + if (DEBUG) { + Log.d(TAG, "submitRequest called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot submit request, configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + } + return mLegacyDevice.submitRequest(request, streaming, lastFrameNumber); + } + + @Override + public int submitRequestList(List<CaptureRequest> request, boolean streaming, + /*out*/LongParcelable lastFrameNumber) { + if (DEBUG) { + Log.d(TAG, "submitRequestList called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot submit request, configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + } + return mLegacyDevice.submitRequestList(request, streaming, lastFrameNumber); + } + + @Override + public int cancelRequest(int requestId, /*out*/LongParcelable lastFrameNumber) { + if (DEBUG) { + Log.d(TAG, "cancelRequest called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot cancel request, configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + } + long lastFrame = mLegacyDevice.cancelRequest(requestId); + lastFrameNumber.setNumber(lastFrame); + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int beginConfigure() { + if (DEBUG) { + Log.d(TAG, "beginConfigure called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot begin configure, configuration change already in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + mConfiguring = true; + } + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int endConfigure() { + if (DEBUG) { + Log.d(TAG, "endConfigure called."); + } + ArrayList<Surface> surfaces = null; + synchronized(mConfigureLock) { + if (!mConfiguring) { + Log.e(TAG, "Cannot end configure, no configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + int numSurfaces = mSurfaces.size(); + if (numSurfaces > 0) { + surfaces = new ArrayList<Surface>(); + for (int i = 0; i < numSurfaces; ++i) { + surfaces.add(mSurfaces.valueAt(i)); + } + } + mConfiguring = false; + } + return mLegacyDevice.configureOutputs(surfaces); + } + + @Override + public int deleteStream(int streamId) { + if (DEBUG) { + Log.d(TAG, "deleteStream called."); + } + synchronized(mConfigureLock) { + if (!mConfiguring) { + Log.e(TAG, "Cannot delete stream, beginConfigure hasn't been called yet."); + return CameraBinderDecorator.INVALID_OPERATION; + } + int index = mSurfaces.indexOfKey(streamId); + if (index < 0) { + Log.e(TAG, "Cannot delete stream, stream id " + streamId + " doesn't exist."); + return CameraBinderDecorator.BAD_VALUE; + } + mSurfaces.removeAt(index); + } + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int createStream(int width, int height, int format, Surface surface) { + if (DEBUG) { + Log.d(TAG, "createStream called."); + } + synchronized(mConfigureLock) { + if (!mConfiguring) { + Log.e(TAG, "Cannot create stream, beginConfigure hasn't been called yet."); + return CameraBinderDecorator.INVALID_OPERATION; + } + int id = ++mSurfaceIdCounter; + mSurfaces.put(id, surface); + return id; + } + } + + @Override + public int createDefaultRequest(int templateId, /*out*/CameraMetadataNative request) { + if (DEBUG) { + Log.d(TAG, "createDefaultRequest called."); + } + // TODO: implement createDefaultRequest. + Log.e(TAG, "createDefaultRequest unimplemented."); + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int getCameraInfo(/*out*/CameraMetadataNative info) { + if (DEBUG) { + Log.d(TAG, "getCameraInfo called."); + } + // TODO: implement getCameraInfo. + Log.e(TAG, "getCameraInfo unimplemented."); + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int waitUntilIdle() throws RemoteException { + if (DEBUG) { + Log.d(TAG, "waitUntilIdle called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot wait until idle, configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + } + mLegacyDevice.waitUntilIdle(); + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public int flush(/*out*/LongParcelable lastFrameNumber) { + if (DEBUG) { + Log.d(TAG, "flush called."); + } + synchronized(mConfigureLock) { + if (mConfiguring) { + Log.e(TAG, "Cannot flush, configuration change in progress."); + return CameraBinderDecorator.INVALID_OPERATION; + } + } + // TODO: implement flush. + return CameraBinderDecorator.NO_ERROR; + } + + @Override + public IBinder asBinder() { + // This is solely intended to be used for in-process binding. + return null; + } +} diff --git a/core/java/android/hardware/camera2/legacy/GLThreadManager.java b/core/java/android/hardware/camera2/legacy/GLThreadManager.java new file mode 100644 index 0000000..3fd2309 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/GLThreadManager.java @@ -0,0 +1,234 @@ +/* + * 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.legacy; + +import android.graphics.SurfaceTexture; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.Surface; + +import java.util.Collection; + +/** + * GLThreadManager handles the thread used for rendering into the configured output surfaces. + */ +public class GLThreadManager { + private final String TAG; + private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + + private static final int MSG_NEW_CONFIGURATION = 1; + private static final int MSG_NEW_FRAME = 2; + private static final int MSG_CLEANUP = 3; + private static final int MSG_DROP_FRAMES = 4; + private static final int MSG_ALLOW_FRAMES = 5; + + private final SurfaceTextureRenderer mTextureRenderer; + + private final RequestHandlerThread mGLHandlerThread; + + private final RequestThreadManager.FpsCounter mPrevCounter = + new RequestThreadManager.FpsCounter("GL Preview Producer"); + + /** + * Container object for Configure messages. + */ + private static class ConfigureHolder { + public final ConditionVariable condition; + public final Collection<Surface> surfaces; + + public ConfigureHolder(ConditionVariable condition, Collection<Surface> surfaces) { + this.condition = condition; + this.surfaces = surfaces; + } + } + + private final Handler.Callback mGLHandlerCb = new Handler.Callback() { + private boolean mCleanup = false; + private boolean mConfigured = false; + private boolean mDroppingFrames = false; + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + if (mCleanup) { + return true; + } + switch (msg.what) { + case MSG_NEW_CONFIGURATION: + ConfigureHolder configure = (ConfigureHolder) msg.obj; + mTextureRenderer.cleanupEGLContext(); + mTextureRenderer.configureSurfaces(configure.surfaces); + configure.condition.open(); + mConfigured = true; + break; + case MSG_NEW_FRAME: + if (mDroppingFrames) { + Log.w(TAG, "Ignoring frame."); + break; + } + if (DEBUG) { + mPrevCounter.countAndLog(); + } + if (!mConfigured) { + Log.e(TAG, "Dropping frame, EGL context not configured!"); + } + mTextureRenderer.drawIntoSurfaces((Collection<Surface>) msg.obj); + break; + case MSG_CLEANUP: + mTextureRenderer.cleanupEGLContext(); + mCleanup = true; + mConfigured = false; + break; + case MSG_DROP_FRAMES: + mDroppingFrames = true; + break; + case MSG_ALLOW_FRAMES: + mDroppingFrames = false; + default: + Log.e(TAG, "Unhandled message " + msg.what + " on GLThread."); + break; + } + return true; + } + }; + + /** + * Create a new GL thread and renderer. + * + * @param cameraId the camera id for this thread. + */ + public GLThreadManager(int cameraId) { + mTextureRenderer = new SurfaceTextureRenderer(); + TAG = String.format("CameraDeviceGLThread-%d", cameraId); + mGLHandlerThread = new RequestHandlerThread(TAG, mGLHandlerCb); + } + + /** + * Start the thread. + * + * <p> + * This must be called before queueing new frames. + * </p> + */ + public void start() { + mGLHandlerThread.start(); + } + + /** + * Wait until the thread has started. + */ + public void waitUntilStarted() { + mGLHandlerThread.waitUntilStarted(); + } + + /** + * Quit the thread. + * + * <p> + * No further methods can be called after this. + * </p> + */ + public void quit() { + Handler handler = mGLHandlerThread.getHandler(); + handler.sendMessageAtFrontOfQueue(handler.obtainMessage(MSG_CLEANUP)); + mGLHandlerThread.quitSafely(); + } + + /** + * Queue a new call to draw into a given set of surfaces. + * + * <p> + * The set of surfaces passed here must be a subset of the set of surfaces passed in + * the last call to {@link #setConfigurationAndWait}. + * </p> + * + * @param targets a collection of {@link android.view.Surface}s to draw into. + */ + public void queueNewFrame(Collection<Surface> targets) { + Handler handler = mGLHandlerThread.getHandler(); + + /** + * Avoid queuing more than one new frame. If we are not consuming faster than frames + * are produced, drop frames rather than allowing the queue to back up. + */ + if (!handler.hasMessages(MSG_NEW_FRAME)) { + handler.sendMessage(handler.obtainMessage(MSG_NEW_FRAME, targets)); + } else { + Log.e(TAG, "GLThread dropping frame. Not consuming frames quickly enough!"); + } + } + + /** + * Configure the GL renderer for the given set of output surfaces, and block until + * this configuration has been applied. + * + * @param surfaces a collection of {@link android.view.Surface}s to configure. + */ + public void setConfigurationAndWait(Collection<Surface> surfaces) { + Handler handler = mGLHandlerThread.getHandler(); + + final ConditionVariable condition = new ConditionVariable(/*closed*/false); + ConfigureHolder configure = new ConfigureHolder(condition, surfaces); + + Message m = handler.obtainMessage(MSG_NEW_CONFIGURATION, /*arg1*/0, /*arg2*/0, configure); + handler.sendMessage(m); + + // Block until configuration applied. + condition.block(); + } + + /** + * Get the underlying surface to produce frames from. + * + * <p> + * This returns the surface that is drawn into the set of surfaces passed in for each frame. + * This method should only be called after a call to + * {@link #setConfigurationAndWait(java.util.Collection)}. Calling this before the first call + * to {@link #setConfigurationAndWait(java.util.Collection)}, after {@link #quit()}, or + * concurrently to one of these calls may result in an invalid + * {@link android.graphics.SurfaceTexture} being returned. + * </p> + * + * @return an {@link android.graphics.SurfaceTexture} to draw to. + */ + public SurfaceTexture getCurrentSurfaceTexture() { + return mTextureRenderer.getSurfaceTexture(); + } + + /** + * Ignore any subsequent calls to {@link #queueNewFrame(java.util.Collection)}. + */ + public void ignoreNewFrames() { + mGLHandlerThread.getHandler().sendEmptyMessage(MSG_DROP_FRAMES); + } + + /** + * Wait until no messages are queued. + */ + public void waitUntilIdle() { + mGLHandlerThread.waitUntilIdle(); + } + + /** + * Re-enable drawing new frames after a call to {@link #ignoreNewFrames()}. + */ + public void allowNewFrames() { + mGLHandlerThread.getHandler().sendEmptyMessage(MSG_ALLOW_FRAMES); + } +} diff --git a/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.java new file mode 100644 index 0000000..f9cf905 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/LegacyCameraDevice.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.hardware.camera2.legacy; + +import android.graphics.ImageFormat; +import android.hardware.Camera; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.impl.CaptureResultExtras; +import android.hardware.camera2.ICameraDeviceCallbacks; +import android.hardware.camera2.utils.LongParcelable; +import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.CameraBinderDecorator; +import android.hardware.camera2.utils.CameraRuntimeException; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.RemoteException; +import android.util.Log; +import android.view.Surface; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class emulates the functionality of a Camera2 device using a the old Camera class. + * + * <p> + * There are two main components that are used to implement this: + * - A state machine containing valid Camera2 device states ({@link CameraDeviceState}). + * - A message-queue based pipeline that manages an old Camera class, and executes capture and + * configuration requests. + * </p> + */ +public class LegacyCameraDevice implements AutoCloseable { + public static final String DEBUG_PROP = "HAL1ShimLogging"; + + private final String TAG; + + private final int mCameraId; + private final ICameraDeviceCallbacks mDeviceCallbacks; + private final CameraDeviceState mDeviceState = new CameraDeviceState(); + + private final ConditionVariable mIdle = new ConditionVariable(/*open*/true); + private final AtomicInteger mRequestIdCounter = new AtomicInteger(0); + + private final HandlerThread mCallbackHandlerThread = new HandlerThread("ResultThread"); + private final Handler mCallbackHandler; + private static final int ILLEGAL_VALUE = -1; + + private CaptureResultExtras getExtrasFromRequest(RequestHolder holder) { + if (holder == null) { + return new CaptureResultExtras(ILLEGAL_VALUE, ILLEGAL_VALUE, ILLEGAL_VALUE, + ILLEGAL_VALUE, ILLEGAL_VALUE); + } + return new CaptureResultExtras(holder.getRequestId(), holder.getSubsequeceId(), + /*afTriggerId*/0, /*precaptureTriggerId*/0, holder.getFrameNumber()); + } + + /** + * Listener for the camera device state machine. Calls the appropriate + * {@link ICameraDeviceCallbacks} for each state transition. + */ + private final CameraDeviceState.CameraDeviceStateListener mStateListener = + new CameraDeviceState.CameraDeviceStateListener() { + @Override + public void onError(final int errorCode, RequestHolder holder) { + mIdle.open(); + final CaptureResultExtras extras = getExtrasFromRequest(holder); + try { + mDeviceCallbacks.onCameraError(errorCode, extras); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception during onCameraError callback: ", e); + } + + } + + @Override + public void onConfiguring() { + // Do nothing + } + + @Override + public void onIdle() { + mIdle.open(); + + try { + mDeviceCallbacks.onCameraIdle(); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception during onCameraIdle callback: ", e); + } + } + + @Override + public void onCaptureStarted(RequestHolder holder) { + final CaptureResultExtras extras = getExtrasFromRequest(holder); + + try { + // TODO: Don't fake timestamp + mDeviceCallbacks.onCaptureStarted(extras, System.nanoTime()); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception during onCameraError callback: ", e); + } + + } + + @Override + public void onCaptureResult(CameraMetadataNative result, RequestHolder holder) { + final CaptureResultExtras extras = getExtrasFromRequest(holder); + + try { + // TODO: Don't fake metadata + mDeviceCallbacks.onResultReceived(result, extras); + } catch (RemoteException e) { + Log.e(TAG, "Received remote exception during onCameraError callback: ", e); + } + } + }; + + private final RequestThreadManager mRequestThreadManager; + + /** + * Check if a given surface uses {@link ImageFormat#YUV_420_888} format. + * + * @param s the surface to check. + * @return {@code true} if the surfaces uses {@link ImageFormat#YUV_420_888}. + */ + static boolean needsConversion(Surface s) { + return LegacyCameraDevice.nativeDetectSurfaceType(s) == ImageFormat.YUV_420_888; + } + + /** + * Create a new emulated camera device from a given Camera 1 API camera. + * + * <p> + * The {@link Camera} provided to this constructor must already have been successfully opened, + * and ownership of the provided camera is passed to this object. No further calls to the + * camera methods should be made following this constructor. + * </p> + * + * @param cameraId the id of the camera. + * @param camera an open {@link Camera} device. + * @param callbacks {@link ICameraDeviceCallbacks} callbacks to call for Camera2 API operations. + */ + public LegacyCameraDevice(int cameraId, Camera camera, ICameraDeviceCallbacks callbacks) { + mCameraId = cameraId; + mDeviceCallbacks = callbacks; + TAG = String.format("CameraDevice-%d-LE", mCameraId); + + mCallbackHandlerThread.start(); + mCallbackHandler = new Handler(mCallbackHandlerThread.getLooper()); + mDeviceState.setCameraDeviceCallbacks(mCallbackHandler, mStateListener); + mRequestThreadManager = + new RequestThreadManager(cameraId, camera, mDeviceState); + mRequestThreadManager.start(); + } + + /** + * Configure the device with a set of output surfaces. + * + * @param outputs a list of surfaces to set. + * @return an error code for this binder operation, or {@link CameraBinderDecorator.NO_ERROR} + * on success. + */ + public int configureOutputs(List<Surface> outputs) { + int error = mDeviceState.setConfiguring(); + if (error == CameraBinderDecorator.NO_ERROR) { + mRequestThreadManager.configure(outputs); + error = mDeviceState.setIdle(); + } + return error; + } + + /** + * Submit a burst of capture requests. + * + * @param requestList a list of capture requests to execute. + * @param repeating {@code true} if this burst is repeating. + * @param frameNumber an output argument that contains either the frame number of the last frame + * that will be returned for this request, or the frame number of the last + * frame that will be returned for the current repeating request if this + * burst is set to be repeating. + * @return the request id. + */ + public int submitRequestList(List<CaptureRequest> requestList, boolean repeating, + /*out*/LongParcelable frameNumber) { + // TODO: validate request here + mIdle.close(); + return mRequestThreadManager.submitCaptureRequests(requestList, repeating, + frameNumber); + } + + /** + * Submit a single capture request. + * + * @param request the capture request to execute. + * @param repeating {@code true} if this request is repeating. + * @param frameNumber an output argument that contains either the frame number of the last frame + * that will be returned for this request, or the frame number of the last + * frame that will be returned for the current repeating request if this + * request is set to be repeating. + * @return the request id. + */ + public int submitRequest(CaptureRequest request, boolean repeating, + /*out*/LongParcelable frameNumber) { + ArrayList<CaptureRequest> requestList = new ArrayList<CaptureRequest>(); + requestList.add(request); + return submitRequestList(requestList, repeating, frameNumber); + } + + /** + * Cancel the repeating request with the given request id. + * + * @param requestId the request id of the request to cancel. + * @return the last frame number to be returned from the HAL for the given repeating request, or + * {@code INVALID_FRAME} if none exists. + */ + public long cancelRequest(int requestId) { + return mRequestThreadManager.cancelRepeating(requestId); + } + + /** + * Block until the {@link ICameraDeviceCallbacks#onCameraIdle()} callback is received. + */ + public void waitUntilIdle() { + mIdle.block(); + } + + @Override + public void close() { + mRequestThreadManager.quit(); + mCallbackHandlerThread.quitSafely(); + // TODO: throw IllegalStateException in every method after close has been called + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } catch (CameraRuntimeException e) { + Log.e(TAG, "Got error while trying to finalize, ignoring: " + e.getMessage()); + } finally { + super.finalize(); + } + } + + protected static native int nativeDetectSurfaceType(Surface surface); + + protected static native void nativeDetectSurfaceDimens(Surface surface, int[] dimens); + + protected static native void nativeConfigureSurface(Surface surface, int width, int height, + int pixelFormat); + + protected static native void nativeProduceFrame(Surface surface, byte[] pixelBuffer, int width, + int height, int pixelFormat); + + protected static native void nativeSetSurfaceFormat(Surface surface, int pixelFormat); + + protected static native void nativeSetSurfaceDimens(Surface surface, int width, int height); + +} diff --git a/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java b/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java new file mode 100644 index 0000000..36cd907 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/RequestHandlerThread.java @@ -0,0 +1,101 @@ +/* + * 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.legacy; + +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.MessageQueue; + +public class RequestHandlerThread extends HandlerThread { + private final ConditionVariable mStarted = new ConditionVariable(false); + private final ConditionVariable mIdle = new ConditionVariable(true); + private Handler.Callback mCallback; + private volatile Handler mHandler; + + public RequestHandlerThread(String name, Handler.Callback callback) { + super(name, Thread.MAX_PRIORITY); + mCallback = callback; + } + + @Override + protected void onLooperPrepared() { + mHandler = new Handler(getLooper(), mCallback); + mStarted.open(); + } + + // Blocks until thread has started + public void waitUntilStarted() { + mStarted.block(); + } + + // May return null if the handler is not set up yet. + public Handler getHandler() { + return mHandler; + } + + // Blocks until thread has started + public Handler waitAndGetHandler() { + waitUntilStarted(); + return getHandler(); + } + + // Atomic multi-type message existence check + public boolean hasAnyMessages(int[] what) { + synchronized (mHandler.getLooper().getQueue()) { + for (int i : what) { + if (mHandler.hasMessages(i)) { + return true; + } + } + } + return false; + } + + // Atomic multi-type message remove + public void removeMessages(int[] what) { + synchronized (mHandler.getLooper().getQueue()) { + for (int i : what) { + mHandler.removeMessages(i); + } + } + } + + private final MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() { + @Override + public boolean queueIdle() { + mIdle.open(); + return false; + } + }; + + // Blocks until thread is idling + public void waitUntilIdle() { + Looper looper = waitAndGetHandler().getLooper(); + if (looper.isIdling()) { + return; + } + mIdle.close(); + looper.getQueue().addIdleHandler(mIdleHandler); + if (looper.isIdling()) { + return; + } + mIdle.block(); + } + +} diff --git a/core/java/android/hardware/camera2/legacy/RequestHolder.java b/core/java/android/hardware/camera2/legacy/RequestHolder.java new file mode 100644 index 0000000..8a9052f --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/RequestHolder.java @@ -0,0 +1,159 @@ +/* + * 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.legacy; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.impl.CameraMetadataNative; +import android.view.Surface; + +import java.util.Collection; + +/** + * Immutable container for a single capture request and associated information. + */ +public class RequestHolder { + + private final boolean mRepeating; + private final CaptureRequest mRequest; + private final int mRequestId; + private final int mSubsequeceId; + private final long mFrameNumber; + + RequestHolder(int requestId, int subsequenceId, CaptureRequest request, boolean repeating, + long frameNumber) { + mRepeating = repeating; + mRequest = request; + mRequestId = requestId; + mSubsequeceId = subsequenceId; + mFrameNumber = frameNumber; + } + + /** + * Return the request id for the contained {@link CaptureRequest}. + */ + public int getRequestId() { + return mRequestId; + } + + /** + * Returns true if the contained request is repeating. + */ + public boolean isRepeating() { + return mRepeating; + } + + /** + * Return the subsequence id for this request. + */ + public int getSubsequeceId() { + return mSubsequeceId; + } + + /** + * Returns the frame number for this request. + */ + public long getFrameNumber() { + return mFrameNumber; + } + + /** + * Returns the contained request. + */ + public CaptureRequest getRequest() { + return mRequest; + } + + /** + * Returns a read-only collection of the surfaces targeted by the contained request. + */ + public Collection<Surface> getHolderTargets() { + return getRequest().getTargets(); + } + + /** + * Returns true if any of the surfaces targeted by the contained request require jpeg buffers. + */ + public boolean hasJpegTargets() { + for (Surface s : getHolderTargets()) { + if (jpegType(s)) { + return true; + } + } + return false; + } + + /** + * Returns true if any of the surfaces targeted by the contained request require a + * non-jpeg buffer type. + */ + public boolean hasPreviewTargets() { + for (Surface s : getHolderTargets()) { + if (previewType(s)) { + return true; + } + } + return false; + } + + /** + * Return the first surface targeted by the contained request that requires a + * non-jpeg buffer type. + */ + public Surface getFirstPreviewTarget() { + for (Surface s : getHolderTargets()) { + if (previewType(s)) { + return s; + } + } + return null; + } + + /** + * Returns true if the given surface requires jpeg buffers. + * + * @param s a {@link Surface} to check. + * @return true if the surface requires a jpeg buffer. + */ + public static boolean jpegType(Surface s) { + if (LegacyCameraDevice.nativeDetectSurfaceType(s) == + CameraMetadataNative.NATIVE_JPEG_FORMAT) { + return true; + } + return false; + } + + /** + * Returns true if the given surface requires non-jpeg buffer types. + * + * <p> + * "Jpeg buffer" refers to the buffers returned in the jpeg + * {@link android.hardware.Camera.PictureCallback}. Non-jpeg buffers are created using a tee + * of the preview stream drawn to the surface + * set via {@link android.hardware.Camera#setPreviewDisplay(android.view.SurfaceHolder)} or + * equivalent methods. + * </p> + * @param s a {@link Surface} to check. + * @return true if the surface requires a non-jpeg buffer type. + */ + public static boolean previewType(Surface s) { + if (LegacyCameraDevice.nativeDetectSurfaceType(s) != + CameraMetadataNative.NATIVE_JPEG_FORMAT) { + return true; + } + return false; + } +} diff --git a/core/java/android/hardware/camera2/legacy/RequestQueue.java b/core/java/android/hardware/camera2/legacy/RequestQueue.java new file mode 100644 index 0000000..5c68303 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/RequestQueue.java @@ -0,0 +1,132 @@ +/* + * 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.legacy; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.utils.LongParcelable; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayDeque; +import java.util.List; + +/** + * A queue of bursts of requests. + * + * <p>This queue maintains the count of frames that have been produced, and is thread safe.</p> + */ +public class RequestQueue { + private static final String TAG = "RequestQueue"; + + private static final long INVALID_FRAME = -1; + + private BurstHolder mRepeatingRequest = null; + private final ArrayDeque<BurstHolder> mRequestQueue = new ArrayDeque<BurstHolder>(); + + private long mCurrentFrameNumber = 0; + private long mCurrentRepeatingFrameNumber = INVALID_FRAME; + private int mCurrentRequestId = 0; + + public RequestQueue() {} + + /** + * Return and remove the next burst on the queue. + * + * <p>If a repeating burst is returned, it will not be removed.</p> + * + * @return a pair containing the next burst and the current frame number, or null if none exist. + */ + public synchronized Pair<BurstHolder, Long> getNext() { + BurstHolder next = mRequestQueue.poll(); + if (next == null && mRepeatingRequest != null) { + next = mRepeatingRequest; + mCurrentRepeatingFrameNumber = mCurrentFrameNumber + + next.getNumberOfRequests(); + } + + if (next == null) { + return null; + } + + Pair<BurstHolder, Long> ret = new Pair<BurstHolder, Long>(next, mCurrentFrameNumber); + mCurrentFrameNumber += next.getNumberOfRequests(); + return ret; + } + + /** + * Cancel a repeating request. + * + * @param requestId the id of the repeating request to cancel. + * @return the last frame to be returned from the HAL for the given repeating request, or + * {@code INVALID_FRAME} if none exists. + */ + public synchronized long stopRepeating(int requestId) { + long ret = INVALID_FRAME; + if (mRepeatingRequest != null && mRepeatingRequest.getRequestId() == requestId) { + mRepeatingRequest = null; + ret = mCurrentRepeatingFrameNumber; + mCurrentRepeatingFrameNumber = INVALID_FRAME; + } else { + Log.e(TAG, "cancel failed: no repeating request exists for request id: " + requestId); + } + return ret; + } + + /** + * Add a the given burst to the queue. + * + * <p>If the burst is repeating, replace the current repeating burst.</p> + * + * @param requests the burst of requests to add to the queue. + * @param repeating true if the burst is repeating. + * @param frameNumber an output argument that contains either the frame number of the last frame + * that will be returned for this request, or the frame number of the last + * frame that will be returned for the current repeating request if this + * burst is set to be repeating. + * @return the request id. + */ + public synchronized int submit(List<CaptureRequest> requests, boolean repeating, + /*out*/LongParcelable frameNumber) { + int requestId = mCurrentRequestId++; + BurstHolder burst = new BurstHolder(requestId, repeating, requests); + long ret = INVALID_FRAME; + if (burst.isRepeating()) { + if (mRepeatingRequest != null) { + ret = mCurrentRepeatingFrameNumber; + } + mCurrentRepeatingFrameNumber = INVALID_FRAME; + mRepeatingRequest = burst; + } else { + mRequestQueue.offer(burst); + ret = calculateLastFrame(burst.getRequestId()); + } + frameNumber.setNumber(ret); + return requestId; + } + + private long calculateLastFrame(int requestId) { + long total = mCurrentFrameNumber; + for (BurstHolder b : mRequestQueue) { + total += b.getNumberOfRequests(); + if (b.getRequestId() == requestId) { + return total; + } + } + throw new IllegalStateException( + "At least one request must be in the queue to calculate frame number"); + } + +} diff --git a/core/java/android/hardware/camera2/legacy/RequestThreadManager.java b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java new file mode 100644 index 0000000..c4669f5 --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/RequestThreadManager.java @@ -0,0 +1,491 @@ +/* + * 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.legacy; + +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.utils.LongParcelable; +import android.hardware.camera2.impl.CameraMetadataNative; +import android.os.ConditionVariable; +import android.os.Handler; +import android.os.Message; +import android.os.SystemClock; +import android.util.Log; +import android.util.Pair; +import android.view.Surface; + +import java.io.IOError; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * This class executes requests to the {@link Camera}. + * + * <p> + * The main components of this class are: + * - A message queue of requests to the {@link Camera}. + * - A thread that consumes requests to the {@link Camera} and executes them. + * - A {@link GLThreadManager} that draws to the configured output {@link Surface}s. + * - An {@link CameraDeviceState} state machine that manages the callbacks for various operations. + * </p> + */ +public class RequestThreadManager { + private final String TAG; + private final int mCameraId; + private final RequestHandlerThread mRequestThread; + + private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + private final Camera mCamera; + + private final CameraDeviceState mDeviceState; + + private static final int MSG_CONFIGURE_OUTPUTS = 1; + private static final int MSG_SUBMIT_CAPTURE_REQUEST = 2; + private static final int MSG_CLEANUP = 3; + + private static final int PREVIEW_FRAME_TIMEOUT = 300; // ms + private static final int JPEG_FRAME_TIMEOUT = 1000; // ms + + private boolean mPreviewRunning = false; + + private volatile RequestHolder mInFlightPreview; + private volatile RequestHolder mInFlightJpeg; + + private List<Surface> mPreviewOutputs = new ArrayList<Surface>(); + private List<Surface> mCallbackOutputs = new ArrayList<Surface>(); + private GLThreadManager mGLThreadManager; + private SurfaceTexture mPreviewTexture; + + private final RequestQueue mRequestQueue = new RequestQueue(); + private SurfaceTexture mDummyTexture; + private Surface mDummySurface; + + private final FpsCounter mPrevCounter = new FpsCounter("Incoming Preview"); + + /** + * Container object for Configure messages. + */ + private static class ConfigureHolder { + public final ConditionVariable condition; + public final Collection<Surface> surfaces; + + public ConfigureHolder(ConditionVariable condition, Collection<Surface> surfaces) { + this.condition = condition; + this.surfaces = surfaces; + } + } + + /** + * Counter class used to calculate and log the current FPS of frame production. + */ + public static class FpsCounter { + //TODO: Hook this up to SystTrace? + private static final String TAG = "FpsCounter"; + private int mFrameCount = 0; + private long mLastTime = 0; + private long mLastPrintTime = 0; + private double mLastFps = 0; + private final String mStreamType; + private static final long NANO_PER_SECOND = 1000000000; //ns + + public FpsCounter(String streamType) { + mStreamType = streamType; + } + + public synchronized void countFrame() { + mFrameCount++; + long nextTime = SystemClock.elapsedRealtimeNanos(); + if (mLastTime == 0) { + mLastTime = nextTime; + } + if (nextTime > mLastTime + NANO_PER_SECOND) { + long elapsed = nextTime - mLastTime; + mLastFps = mFrameCount * (NANO_PER_SECOND / (double) elapsed); + mFrameCount = 0; + mLastTime = nextTime; + } + } + + public synchronized double checkFps() { + return mLastFps; + } + + public synchronized void staggeredLog() { + if (mLastTime > mLastPrintTime + 5 * NANO_PER_SECOND) { + mLastPrintTime = mLastTime; + Log.d(TAG, "FPS for " + mStreamType + " stream: " + mLastFps ); + } + } + + public synchronized void countAndLog() { + countFrame(); + staggeredLog(); + } + } + /** + * Fake preview for jpeg captures when there is no active preview + */ + private void createDummySurface() { + if (mDummyTexture == null || mDummySurface == null) { + mDummyTexture = new SurfaceTexture(/*ignored*/0); + // TODO: use smallest default sizes + mDummyTexture.setDefaultBufferSize(640, 480); + mDummySurface = new Surface(mDummyTexture); + } + } + + private final ConditionVariable mReceivedJpeg = new ConditionVariable(false); + private final ConditionVariable mReceivedPreview = new ConditionVariable(false); + + private final Camera.PictureCallback mJpegCallback = new Camera.PictureCallback() { + @Override + public void onPictureTaken(byte[] data, Camera camera) { + Log.i(TAG, "Received jpeg."); + RequestHolder holder = mInFlightJpeg; + if (holder == null) { + Log.w(TAG, "Dropping jpeg frame."); + mInFlightJpeg = null; + return; + } + for (Surface s : holder.getHolderTargets()) { + if (RequestHolder.jpegType(s)) { + Log.i(TAG, "Producing jpeg buffer..."); + LegacyCameraDevice.nativeSetSurfaceDimens(s, data.length, /*height*/1); + LegacyCameraDevice.nativeProduceFrame(s, data, data.length, /*height*/1, + CameraMetadataNative.NATIVE_JPEG_FORMAT); + } + } + mReceivedJpeg.open(); + } + }; + + private final SurfaceTexture.OnFrameAvailableListener mPreviewCallback = + new SurfaceTexture.OnFrameAvailableListener() { + @Override + public void onFrameAvailable(SurfaceTexture surfaceTexture) { + if (DEBUG) { + mPrevCounter.countAndLog(); + } + RequestHolder holder = mInFlightPreview; + if (holder == null) { + Log.w(TAG, "Dropping preview frame."); + mInFlightPreview = null; + return; + } + if (holder.hasPreviewTargets()) { + mGLThreadManager.queueNewFrame(holder.getHolderTargets()); + } + + mReceivedPreview.open(); + } + }; + + private void stopPreview() { + if (mPreviewRunning) { + mCamera.stopPreview(); + mPreviewRunning = false; + } + } + + private void startPreview() { + if (!mPreviewRunning) { + mCamera.startPreview(); + mPreviewRunning = true; + } + } + + private void doJpegCapture(RequestHolder request) throws IOException { + if (!mPreviewRunning) { + createDummySurface(); + mCamera.setPreviewTexture(mDummyTexture); + startPreview(); + } + mInFlightJpeg = request; + // TODO: Hook up shutter callback to CameraDeviceStateListener#onCaptureStarted + mCamera.takePicture(/*shutter*/null, /*raw*/null, mJpegCallback); + mPreviewRunning = false; + } + + private void doPreviewCapture(RequestHolder request) throws IOException { + mInFlightPreview = request; + if (mPreviewRunning) { + return; // Already running + } + + mPreviewTexture.setDefaultBufferSize(640, 480); // TODO: size selection based on request + mCamera.setPreviewTexture(mPreviewTexture); + Camera.Parameters params = mCamera.getParameters(); + List<int[]> supportedFpsRanges = params.getSupportedPreviewFpsRange(); + int[] bestRange = getPhotoPreviewFpsRange(supportedFpsRanges); + if (DEBUG) { + Log.d(TAG, "doPreviewCapture - Selected range [" + + bestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX] + "," + + bestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX] + "]"); + } + params.setPreviewFpsRange(bestRange[Camera.Parameters.PREVIEW_FPS_MIN_INDEX], + bestRange[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]); + params.setRecordingHint(true); + mCamera.setParameters(params); + + startPreview(); + } + + private void configureOutputs(Collection<Surface> outputs) throws IOException { + stopPreview(); + if (mGLThreadManager != null) { + mGLThreadManager.waitUntilStarted(); + mGLThreadManager.ignoreNewFrames(); + mGLThreadManager.waitUntilIdle(); + } + mPreviewOutputs.clear(); + mCallbackOutputs.clear(); + mPreviewTexture = null; + mInFlightPreview = null; + mInFlightJpeg = null; + + for (Surface s : outputs) { + int format = LegacyCameraDevice.nativeDetectSurfaceType(s); + switch (format) { + case CameraMetadataNative.NATIVE_JPEG_FORMAT: + mCallbackOutputs.add(s); + break; + default: + mPreviewOutputs.add(s); + break; + } + } + + // TODO: Detect and optimize single-output paths here to skip stream teeing. + if (mGLThreadManager == null) { + mGLThreadManager = new GLThreadManager(mCameraId); + mGLThreadManager.start(); + } + mGLThreadManager.waitUntilStarted(); + mGLThreadManager.setConfigurationAndWait(mPreviewOutputs); + mGLThreadManager.allowNewFrames(); + mPreviewTexture = mGLThreadManager.getCurrentSurfaceTexture(); + mPreviewTexture.setOnFrameAvailableListener(mPreviewCallback); + } + + // Calculate the highest FPS range supported + private int[] getPhotoPreviewFpsRange(List<int[]> frameRates) { + if (frameRates.size() == 0) { + Log.e(TAG, "No supported frame rates returned!"); + return null; + } + + int bestMin = 0; + int bestMax = 0; + int bestIndex = 0; + int index = 0; + for (int[] rate : frameRates) { + int minFps = rate[Camera.Parameters.PREVIEW_FPS_MIN_INDEX]; + int maxFps = rate[Camera.Parameters.PREVIEW_FPS_MAX_INDEX]; + if (maxFps > bestMax || (maxFps == bestMax && minFps > bestMin)) { + bestMin = minFps; + bestMax = maxFps; + bestIndex = index; + } + index++; + } + + return frameRates.get(bestIndex); + } + + private final Handler.Callback mRequestHandlerCb = new Handler.Callback() { + private boolean mCleanup = false; + private List<RequestHolder> mRepeating = null; + + @SuppressWarnings("unchecked") + @Override + public boolean handleMessage(Message msg) { + if (mCleanup) { + return true; + } + + switch (msg.what) { + case MSG_CONFIGURE_OUTPUTS: + ConfigureHolder config = (ConfigureHolder) msg.obj; + Log.i(TAG, "Configure outputs: " + config.surfaces.size() + + " surfaces configured."); + try { + configureOutputs(config.surfaces); + } catch (IOException e) { + // TODO: report error to CameraDevice + throw new IOError(e); + } + config.condition.open(); + break; + case MSG_SUBMIT_CAPTURE_REQUEST: + Handler handler = RequestThreadManager.this.mRequestThread.getHandler(); + + // Get the next burst from the request queue. + Pair<BurstHolder, Long> nextBurst = mRequestQueue.getNext(); + if (nextBurst == null) { + mDeviceState.setIdle(); + stopPreview(); + break; + } else { + // Queue another capture if we did not get the last burst. + handler.sendEmptyMessage(MSG_SUBMIT_CAPTURE_REQUEST); + } + + // Complete each request in the burst + List<RequestHolder> requests = + nextBurst.first.produceRequestHolders(nextBurst.second); + for (RequestHolder holder : requests) { + mDeviceState.setCaptureStart(holder); + try { + if (holder.hasPreviewTargets()) { + mReceivedPreview.close(); + doPreviewCapture(holder); + if (!mReceivedPreview.block(PREVIEW_FRAME_TIMEOUT)) { + // TODO: report error to CameraDevice + Log.e(TAG, "Hit timeout for preview callback!"); + } + } + if (holder.hasJpegTargets()) { + mReceivedJpeg.close(); + doJpegCapture(holder); + mReceivedJpeg.block(); + if (!mReceivedJpeg.block(JPEG_FRAME_TIMEOUT)) { + // TODO: report error to CameraDevice + Log.e(TAG, "Hit timeout for jpeg callback!"); + } + mInFlightJpeg = null; + } + } catch (IOException e) { + // TODO: err handling + throw new IOError(e); + } + // TODO: Set fields in result. + mDeviceState.setCaptureResult(holder, new CameraMetadataNative()); + } + break; + case MSG_CLEANUP: + mCleanup = true; + if (mGLThreadManager != null) { + mGLThreadManager.quit(); + } + if (mCamera != null) { + mCamera.release(); + } + break; + default: + throw new AssertionError("Unhandled message " + msg.what + + " on RequestThread."); + } + return true; + } + }; + + /** + * Create a new RequestThreadManager. + * + * @param cameraId the id of the camera to use. + * @param camera an open camera object. The RequestThreadManager takes ownership of this camera + * object, and is responsible for closing it. + * @param deviceState a {@link CameraDeviceState} state machine. + */ + public RequestThreadManager(int cameraId, Camera camera, + CameraDeviceState deviceState) { + mCamera = camera; + mCameraId = cameraId; + String name = String.format("RequestThread-%d", cameraId); + TAG = name; + mDeviceState = deviceState; + mRequestThread = new RequestHandlerThread(name, mRequestHandlerCb); + } + + /** + * Start the request thread. + */ + public void start() { + mRequestThread.start(); + } + + /** + * Flush the pending requests. + */ + public void flush() { + // TODO: Implement flush. + Log.e(TAG, "flush not yet implemented."); + } + + /** + * Quit the request thread, and clean up everything. + */ + public void quit() { + Handler handler = mRequestThread.waitAndGetHandler(); + handler.sendMessageAtFrontOfQueue(handler.obtainMessage(MSG_CLEANUP)); + mRequestThread.quitSafely(); + } + + /** + * Submit the given burst of requests to be captured. + * + * <p>If the burst is repeating, replace the current repeating burst.</p> + * + * @param requests the burst of requests to add to the queue. + * @param repeating true if the burst is repeating. + * @param frameNumber an output argument that contains either the frame number of the last frame + * that will be returned for this request, or the frame number of the last + * frame that will be returned for the current repeating request if this + * burst is set to be repeating. + * @return the request id. + */ + public int submitCaptureRequests(List<CaptureRequest> requests, boolean repeating, + /*out*/LongParcelable frameNumber) { + Handler handler = mRequestThread.waitAndGetHandler(); + int ret = mRequestQueue.submit(requests, repeating, frameNumber); + handler.sendEmptyMessage(MSG_SUBMIT_CAPTURE_REQUEST); + return ret; + } + + /** + * Cancel a repeating request. + * + * @param requestId the id of the repeating request to cancel. + * @return the last frame to be returned from the HAL for the given repeating request, or + * {@code INVALID_FRAME} if none exists. + */ + public long cancelRepeating(int requestId) { + return mRequestQueue.stopRepeating(requestId); + } + + + /** + * Configure with the current output Surfaces. + * + * <p> + * This operation blocks until the configuration is complete. + * </p> + * + * @param outputs a {@link java.util.Collection} of outputs to configure. + */ + public void configure(Collection<Surface> outputs) { + Handler handler = mRequestThread.waitAndGetHandler(); + final ConditionVariable condition = new ConditionVariable(/*closed*/false); + ConfigureHolder holder = new ConfigureHolder(condition, outputs); + handler.sendMessage(handler.obtainMessage(MSG_CONFIGURE_OUTPUTS, 0, 0, holder)); + condition.block(); + } +} diff --git a/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java b/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java new file mode 100644 index 0000000..2f0f6bc --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/SurfaceTextureRenderer.java @@ -0,0 +1,522 @@ +/* +* 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.legacy; + +import android.graphics.ImageFormat; +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.opengl.Matrix; +import android.util.Log; +import android.view.Surface; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A renderer class that manages the GL state, and can draw a frame into a set of output + * {@link Surface}s. + */ +public class SurfaceTextureRenderer { + private static final String TAG = SurfaceTextureRenderer.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(LegacyCameraDevice.DEBUG_PROP, Log.DEBUG); + private static final int EGL_RECORDABLE_ANDROID = 0x3142; // from EGL/eglext.h + private static final int GL_MATRIX_SIZE = 16; + private static final int VERTEX_POS_SIZE = 3; + private static final int VERTEX_UV_SIZE = 2; + private static final int EGL_COLOR_BITLENGTH = 8; + private static final int GLES_VERSION = 2; + private static final int PBUFFER_PIXEL_BYTES = 4; + + private EGLDisplay mEGLDisplay = EGL14.EGL_NO_DISPLAY; + private EGLContext mEGLContext = EGL14.EGL_NO_CONTEXT; + private EGLConfig mConfigs; + + private class EGLSurfaceHolder { + Surface surface; + EGLSurface eglSurface; + int width; + int height; + } + + private List<EGLSurfaceHolder> mSurfaces = new ArrayList<EGLSurfaceHolder>(); + private List<EGLSurfaceHolder> mConversionSurfaces = new ArrayList<EGLSurfaceHolder>(); + + private ByteBuffer mPBufferPixels; + + // Hold this to avoid GC + private volatile SurfaceTexture mSurfaceTexture; + + private static final int FLOAT_SIZE_BYTES = 4; + private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES; + private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0; + private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3; + private final float[] mTriangleVerticesData = { + // X, Y, Z, U, V + -1.0f, -1.0f, 0, 0.f, 0.f, + 1.0f, -1.0f, 0, 1.f, 0.f, + -1.0f, 1.0f, 0, 0.f, 1.f, + 1.0f, 1.0f, 0, 1.f, 1.f, + }; + + private FloatBuffer mTriangleVertices; + + /** + * As used in this file, this vertex shader maps a unit square to the view, and + * tells the fragment shader to interpolate over it. Each surface pixel position + * is mapped to a 2D homogeneous texture coordinate of the form (s, t, 0, 1) with + * s and t in the inclusive range [0, 1], and the matrix from + * {@link SurfaceTexture#getTransformMatrix(float[])} is used to map this + * coordinate to a texture location. + */ + private static final String VERTEX_SHADER = + "uniform mat4 uMVPMatrix;\n" + + "uniform mat4 uSTMatrix;\n" + + "attribute vec4 aPosition;\n" + + "attribute vec4 aTextureCoord;\n" + + "varying vec2 vTextureCoord;\n" + + "void main() {\n" + + " gl_Position = uMVPMatrix * aPosition;\n" + + " vTextureCoord = (uSTMatrix * aTextureCoord).xy;\n" + + "}\n"; + + /** + * This fragment shader simply draws the color in the 2D texture at + * the location from the {@code VERTEX_SHADER}. + */ + private static final String FRAGMENT_SHADER = + "#extension GL_OES_EGL_image_external : require\n" + + "precision mediump float;\n" + + "varying vec2 vTextureCoord;\n" + + "uniform samplerExternalOES sTexture;\n" + + "void main() {\n" + + " gl_FragColor = texture2D(sTexture, vTextureCoord);\n" + + "}\n"; + + private float[] mMVPMatrix = new float[GL_MATRIX_SIZE]; + private float[] mSTMatrix = new float[GL_MATRIX_SIZE]; + + private int mProgram; + private int mTextureID = 0; + private int muMVPMatrixHandle; + private int muSTMatrixHandle; + private int maPositionHandle; + private int maTextureHandle; + + public SurfaceTextureRenderer() { + mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * + FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer(); + mTriangleVertices.put(mTriangleVerticesData).position(0); + Matrix.setIdentityM(mSTMatrix, 0); + } + + private int loadShader(int shaderType, String source) { + int shader = GLES20.glCreateShader(shaderType); + checkGlError("glCreateShader type=" + shaderType); + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + int[] compiled = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0); + if (compiled[0] == 0) { + Log.e(TAG, "Could not compile shader " + shaderType + ":"); + Log.e(TAG, " " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + // TODO: handle this more gracefully + throw new IllegalStateException("Could not compile shader " + shaderType); + } + return shader; + } + + private int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (pixelShader == 0) { + return 0; + } + + int program = GLES20.glCreateProgram(); + checkGlError("glCreateProgram"); + if (program == 0) { + Log.e(TAG, "Could not create program"); + } + GLES20.glAttachShader(program, vertexShader); + checkGlError("glAttachShader"); + GLES20.glAttachShader(program, pixelShader); + checkGlError("glAttachShader"); + GLES20.glLinkProgram(program); + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] != GLES20.GL_TRUE) { + Log.e(TAG, "Could not link program: "); + Log.e(TAG, GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + // TODO: handle this more gracefully + throw new IllegalStateException("Could not link program"); + } + return program; + } + + private void drawFrame(SurfaceTexture st) { + checkGlError("onDrawFrame start"); + st.getTransformMatrix(mSTMatrix); + + if (DEBUG) { + GLES20.glClearColor(0.0f, 1.0f, 0.0f, 1.0f); + GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); + } + + GLES20.glUseProgram(mProgram); + checkGlError("glUseProgram"); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET); + GLES20.glVertexAttribPointer(maPositionHandle, VERTEX_POS_SIZE, GLES20.GL_FLOAT, + /*normalized*/ false,TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maPosition"); + GLES20.glEnableVertexAttribArray(maPositionHandle); + checkGlError("glEnableVertexAttribArray maPositionHandle"); + + mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET); + GLES20.glVertexAttribPointer(maTextureHandle, VERTEX_UV_SIZE, GLES20.GL_FLOAT, + /*normalized*/ false, TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices); + checkGlError("glVertexAttribPointer maTextureHandle"); + GLES20.glEnableVertexAttribArray(maTextureHandle); + checkGlError("glEnableVertexAttribArray maTextureHandle"); + + Matrix.setIdentityM(mMVPMatrix, 0); + GLES20.glUniformMatrix4fv(muMVPMatrixHandle, /*count*/ 1, /*transpose*/ false, mMVPMatrix, + /*offset*/ 0); + GLES20.glUniformMatrix4fv(muSTMatrixHandle, /*count*/ 1, /*transpose*/ false, mSTMatrix, + /*offset*/ 0); + + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /*offset*/ 0, /*count*/ 4); + checkGlError("glDrawArrays"); + GLES20.glFinish(); + } + + /** + * Initializes GL state. Call this after the EGL surface has been created and made current. + */ + private void initializeGLState() { + mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); + if (mProgram == 0) { + throw new IllegalStateException("failed creating program"); + } + maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); + checkGlError("glGetAttribLocation aPosition"); + if (maPositionHandle == -1) { + throw new IllegalStateException("Could not get attrib location for aPosition"); + } + maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); + checkGlError("glGetAttribLocation aTextureCoord"); + if (maTextureHandle == -1) { + throw new IllegalStateException("Could not get attrib location for aTextureCoord"); + } + + muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); + checkGlError("glGetUniformLocation uMVPMatrix"); + if (muMVPMatrixHandle == -1) { + throw new IllegalStateException("Could not get attrib location for uMVPMatrix"); + } + + muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); + checkGlError("glGetUniformLocation uSTMatrix"); + if (muSTMatrixHandle == -1) { + throw new IllegalStateException("Could not get attrib location for uSTMatrix"); + } + + int[] textures = new int[1]; + GLES20.glGenTextures(/*n*/ 1, textures, /*offset*/ 0); + + mTextureID = textures[0]; + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureID); + checkGlError("glBindTexture mTextureID"); + + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, + GLES20.GL_NEAREST); + GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, + GLES20.GL_LINEAR); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, + GLES20.GL_CLAMP_TO_EDGE); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, + GLES20.GL_CLAMP_TO_EDGE); + checkGlError("glTexParameter"); + } + + private int getTextureId() { + return mTextureID; + } + + private void clearState() { + mSurfaces.clear(); + mConversionSurfaces.clear(); + mPBufferPixels = null; + mSurfaceTexture = null; + } + + private void configureEGLContext() { + mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { + throw new IllegalStateException("No EGL14 display"); + } + int[] version = new int[2]; + if (!EGL14.eglInitialize(mEGLDisplay, version, /*offset*/ 0, version, /*offset*/ 1)) { + throw new IllegalStateException("Cannot initialize EGL14"); + } + + int[] attribList = { + EGL14.EGL_RED_SIZE, EGL_COLOR_BITLENGTH, + EGL14.EGL_GREEN_SIZE, EGL_COLOR_BITLENGTH, + EGL14.EGL_BLUE_SIZE, EGL_COLOR_BITLENGTH, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL_RECORDABLE_ANDROID, 1, + EGL14.EGL_SURFACE_TYPE, EGL14.EGL_PBUFFER_BIT | EGL14.EGL_WINDOW_BIT, + EGL14.EGL_NONE + }; + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + EGL14.eglChooseConfig(mEGLDisplay, attribList, /*offset*/ 0, configs, /*offset*/ 0, + configs.length, numConfigs, /*offset*/ 0); + checkEglError("eglCreateContext RGB888+recordable ES2"); + mConfigs = configs[0]; + int[] attrib_list = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, GLES_VERSION, + EGL14.EGL_NONE + }; + mEGLContext = EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, + attrib_list, /*offset*/ 0); + checkEglError("eglCreateContext"); + if(mEGLContext == EGL14.EGL_NO_CONTEXT) { + throw new IllegalStateException("No EGLContext could be made"); + } + } + + private void configureEGLOutputSurfaces(Collection<EGLSurfaceHolder> surfaces) { + if (surfaces == null || surfaces.size() == 0) { + throw new IllegalStateException("No Surfaces were provided to draw to"); + } + int[] surfaceAttribs = { + EGL14.EGL_NONE + }; + for (EGLSurfaceHolder holder : surfaces) { + holder.eglSurface = EGL14.eglCreateWindowSurface(mEGLDisplay, mConfigs, holder.surface, + surfaceAttribs, 0); + checkEglError("eglCreateWindowSurface"); + } + } + + private void configureEGLPbufferSurfaces(Collection<EGLSurfaceHolder> surfaces) { + if (surfaces == null || surfaces.size() == 0) { + throw new IllegalStateException("No Surfaces were provided to draw to"); + } + + int maxLength = 0; + int[] dimens = new int[2]; + for (EGLSurfaceHolder holder : surfaces) { + LegacyCameraDevice.nativeDetectSurfaceDimens(holder.surface, dimens); + int length = dimens[0] * dimens[1]; + // Find max surface size, ensure PBuffer can hold this many pixels + maxLength = (length > maxLength) ? length : maxLength; + int[] surfaceAttribs = { + EGL14.EGL_WIDTH, dimens[0], + EGL14.EGL_HEIGHT, dimens[1], + EGL14.EGL_NONE + }; + holder.width = dimens[0]; + holder.height = dimens[1]; + holder.eglSurface = + EGL14.eglCreatePbufferSurface(mEGLDisplay, mConfigs, surfaceAttribs, 0); + checkEglError("eglCreatePbufferSurface"); + } + mPBufferPixels = ByteBuffer.allocateDirect(maxLength * PBUFFER_PIXEL_BYTES) + .order(ByteOrder.nativeOrder()); + } + + private void releaseEGLContext() { + if (mEGLDisplay != EGL14.EGL_NO_DISPLAY) { + EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, + EGL14.EGL_NO_CONTEXT); + if (mSurfaces != null) { + for (EGLSurfaceHolder holder : mSurfaces) { + if (holder.eglSurface != null) { + EGL14.eglDestroySurface(mEGLDisplay, holder.eglSurface); + } + } + } + if (mConversionSurfaces != null) { + for (EGLSurfaceHolder holder : mConversionSurfaces) { + if (holder.eglSurface != null) { + EGL14.eglDestroySurface(mEGLDisplay, holder.eglSurface); + } + } + } + EGL14.eglDestroyContext(mEGLDisplay, mEGLContext); + EGL14.eglReleaseThread(); + EGL14.eglTerminate(mEGLDisplay); + } + + mConfigs = null; + mEGLDisplay = EGL14.EGL_NO_DISPLAY; + mEGLContext = EGL14.EGL_NO_CONTEXT; + clearState(); + } + + private void makeCurrent(EGLSurface surface) { + EGL14.eglMakeCurrent(mEGLDisplay, surface, surface, mEGLContext); + checkEglError("makeCurrent"); + } + + private boolean swapBuffers(EGLSurface surface) { + boolean result = EGL14.eglSwapBuffers(mEGLDisplay, surface); + checkEglError("swapBuffers"); + return result; + } + + private void checkEglError(String msg) { + int error; + if ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { + throw new IllegalStateException(msg + ": EGL error: 0x" + Integer.toHexString(error)); + } + } + + private void checkGlError(String msg) { + int error; + while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { + throw new IllegalStateException(msg + ": GLES20 error: 0x" + Integer.toHexString(error)); + } + } + + /** + * Return the surface texture to draw to - this is the texture use to when producing output + * surface buffers. + * + * @return a {@link SurfaceTexture}. + */ + public SurfaceTexture getSurfaceTexture() { + return mSurfaceTexture; + } + + /** + * Set a collection of output {@link Surface}s that can be drawn to. + * + * @param surfaces a {@link Collection} of surfaces. + */ + public void configureSurfaces(Collection<Surface> surfaces) { + releaseEGLContext(); + + for (Surface s : surfaces) { + // If pixel conversions aren't handled by egl, use a pbuffer + if (LegacyCameraDevice.needsConversion(s)) { + LegacyCameraDevice.nativeSetSurfaceFormat(s, ImageFormat.NV21); + EGLSurfaceHolder holder = new EGLSurfaceHolder(); + holder.surface = s; + mConversionSurfaces.add(holder); + } else { + EGLSurfaceHolder holder = new EGLSurfaceHolder(); + holder.surface = s; + mSurfaces.add(holder); + } + } + + // Set up egl display + configureEGLContext(); + + // Set up regular egl surfaces if needed + if (mSurfaces.size() > 0) { + configureEGLOutputSurfaces(mSurfaces); + } + + // Set up pbuffer surface if needed + if (mConversionSurfaces.size() > 0) { + configureEGLPbufferSurfaces(mConversionSurfaces); + } + makeCurrent((mSurfaces.size() > 0) ? mSurfaces.get(0).eglSurface : + mConversionSurfaces.get(0).eglSurface); + initializeGLState(); + mSurfaceTexture = new SurfaceTexture(getTextureId()); + } + + /** + * Draw the current buffer in the {@link SurfaceTexture} returned from + * {@link #getSurfaceTexture()} into the given set of target surfaces. + * + * <p> + * The given surfaces must be a subset of the surfaces set in the last + * {@link #configureSurfaces(java.util.Collection)} call. + * </p> + * + * @param targetSurfaces the surfaces to draw to. + */ + public void drawIntoSurfaces(Collection<Surface> targetSurfaces) { + if ((mSurfaces == null || mSurfaces.size() == 0) + && (mConversionSurfaces == null || mConversionSurfaces.size() == 0)) { + return; + } + checkGlError("before updateTexImage"); + mSurfaceTexture.updateTexImage(); + for (EGLSurfaceHolder holder : mSurfaces) { + if (targetSurfaces.contains(holder.surface)) { + makeCurrent(holder.eglSurface); + drawFrame(mSurfaceTexture); + swapBuffers(holder.eglSurface); + } + + } + for (EGLSurfaceHolder holder : mConversionSurfaces) { + if (targetSurfaces.contains(holder.surface)) { + makeCurrent(holder.eglSurface); + drawFrame(mSurfaceTexture); + mPBufferPixels.clear(); + GLES20.glReadPixels(/*x*/ 0, /*y*/ 0, holder.width, holder.height, GLES20.GL_RGBA, + GLES20.GL_UNSIGNED_BYTE, mPBufferPixels); + checkGlError("glReadPixels"); + int format = LegacyCameraDevice.nativeDetectSurfaceType(holder.surface); + LegacyCameraDevice.nativeProduceFrame(holder.surface, mPBufferPixels.array(), + holder.width, holder.height, format); + swapBuffers(holder.eglSurface); + } + } + } + + /** + * Clean up the current GL context. + */ + public void cleanupEGLContext() { + releaseEGLContext(); + } + + /** + * Drop all current GL operations on the floor. + */ + public void flush() { + // TODO: implement flush + Log.e(TAG, "Flush not yet implemented."); + } +} diff --git a/core/java/android/hardware/camera2/legacy/package.html b/core/java/android/hardware/camera2/legacy/package.html new file mode 100644 index 0000000..db6f78b --- /dev/null +++ b/core/java/android/hardware/camera2/legacy/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body>
\ No newline at end of file diff --git a/core/java/android/hardware/camera2/marshal/MarshalHelpers.java b/core/java/android/hardware/camera2/marshal/MarshalHelpers.java new file mode 100644 index 0000000..35ecc2a --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/MarshalHelpers.java @@ -0,0 +1,243 @@ +/* + * 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.marshal; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static com.android.internal.util.Preconditions.*; + +import android.hardware.camera2.impl.CameraMetadataNative; +import android.util.Rational; + +/** + * Static functions in order to help implementing various marshaler functionality. + * + * <p>The intention is to statically import everything from this file into another file when + * implementing a new marshaler (or marshal queryable).</p> + * + * <p>The helpers are centered around providing primitive knowledge of the native types, + * such as the native size, the managed class wrappers, and various precondition checks.</p> + */ +public final class MarshalHelpers { + + public static final int SIZEOF_BYTE = 1; + public static final int SIZEOF_INT32 = Integer.SIZE / Byte.SIZE; + public static final int SIZEOF_INT64 = Long.SIZE / Byte.SIZE; + public static final int SIZEOF_FLOAT = Float.SIZE / Byte.SIZE; + public static final int SIZEOF_DOUBLE = Double.SIZE / Byte.SIZE; + public static final int SIZEOF_RATIONAL = SIZEOF_INT32 * 2; + + /** + * Get the size in bytes for the native camera metadata type. + * + * <p>This used to determine how many bytes it would take to encode/decode a single value + * of that {@link nativeType}.</p> + * + * @param nativeType the native type, e.g. + * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE}. + * @return size in bytes >= 1 + * + * @throws UnsupportedOperationException if nativeType was not one of the built-in types + */ + public static int getPrimitiveTypeSize(int nativeType) { + switch (nativeType) { + case TYPE_BYTE: + return SIZEOF_BYTE; + case TYPE_INT32: + return SIZEOF_INT32; + case TYPE_FLOAT: + return SIZEOF_FLOAT; + case TYPE_INT64: + return SIZEOF_INT64; + case TYPE_DOUBLE: + return SIZEOF_DOUBLE; + case TYPE_RATIONAL: + return SIZEOF_RATIONAL; + } + + throw new UnsupportedOperationException("Unknown type, can't get size for " + + nativeType); + } + + + /** + * Ensure that the {@code klass} is one of the metadata-primitive classes. + * + * @param klass a non-{@code null} reference + * @return {@code klass} instance + * + * @throws UnsupportedOperationException if klass was not one of the built-in classes + * @throws NullPointerException if klass was null + * + * @see #isPrimitiveClass + */ + public static <T> Class<T> checkPrimitiveClass(Class<T> klass) { + checkNotNull(klass, "klass must not be null"); + + if (isPrimitiveClass(klass)) { + return klass; + } + + throw new UnsupportedOperationException("Unsupported class '" + klass + + "'; expected a metadata primitive class"); + } + + /** + * Checks whether or not {@code klass} is one of the metadata-primitive classes. + * + * <p>The following types (whether boxed or unboxed) are considered primitive: + * <ul> + * <li>byte + * <li>int + * <li>float + * <li>double + * <li>Rational + * </ul> + * </p> + * + * <p>This doesn't strictly follow the java understanding of primitive since + * boxed objects are included, Rational is included, and other types such as char and + * short are not included.</p> + * + * @param klass a {@link Class} instance; using {@code null} will return {@code false} + * @return {@code true} if primitive, {@code false} otherwise + */ + public static <T> boolean isPrimitiveClass(Class<T> klass) { + if (klass == null) { + return false; + } + + if (klass == byte.class || klass == Byte.class) { + return true; + } else if (klass == int.class || klass == Integer.class) { + return true; + } else if (klass == float.class || klass == Float.class) { + return true; + } else if (klass == long.class || klass == Long.class) { + return true; + } else if (klass == double.class || klass == Double.class) { + return true; + } else if (klass == Rational.class) { + return true; + } + + return false; + } + + /** + * Wrap {@code klass} with its wrapper variant if it was a {@code Class} corresponding + * to a Java primitive. + * + * <p>Non-primitive classes are passed through as-is.</p> + * + * <p>For example, for a primitive {@code int.class => Integer.class}, + * but for a non-primitive {@code Rational.class => Rational.class}.</p> + * + * @param klass a {@code Class} reference + * + * @return wrapped class object, or same class object if non-primitive + */ + @SuppressWarnings("unchecked") + public static <T> Class<T> wrapClassIfPrimitive(Class<T> klass) { + if (klass == byte.class) { + return (Class<T>)Byte.class; + } else if (klass == int.class) { + return (Class<T>)Integer.class; + } else if (klass == float.class) { + return (Class<T>)Float.class; + } else if (klass == long.class) { + return (Class<T>)Long.class; + } else if (klass == double.class) { + return (Class<T>)Double.class; + } + + return klass; + } + + /** + * Return a human-readable representation of the {@code nativeType}, e.g. "TYPE_INT32" + * + * <p>Out-of-range values return a string with "UNKNOWN" as the prefix.</p> + * + * @param nativeType the native type + * + * @return human readable type name + */ + public static String toStringNativeType(int nativeType) { + switch (nativeType) { + case TYPE_BYTE: + return "TYPE_BYTE"; + case TYPE_INT32: + return "TYPE_INT32"; + case TYPE_FLOAT: + return "TYPE_FLOAT"; + case TYPE_INT64: + return "TYPE_INT64"; + case TYPE_DOUBLE: + return "TYPE_DOUBLE"; + case TYPE_RATIONAL: + return "TYPE_RATIONAL"; + } + + return "UNKNOWN(" + nativeType + ")"; + } + + /** + * Ensure that the {@code nativeType} is one of the native types supported + * by {@link CameraMetadataNative}. + * + * @param nativeType the native type + * + * @return the native type + * + * @throws UnsupportedOperationException if the native type was invalid + */ + public static int checkNativeType(int nativeType) { + switch (nativeType) { + case TYPE_BYTE: + case TYPE_INT32: + case TYPE_FLOAT: + case TYPE_INT64: + case TYPE_DOUBLE: + case TYPE_RATIONAL: + return nativeType; + } + + throw new UnsupportedOperationException("Unknown nativeType " + nativeType); + } + + /** + * Ensure that the expected and actual native types are equal. + * + * @param expectedNativeType the expected native type + * @param actualNativeType the actual native type + * @return the actual native type + * + * @throws UnsupportedOperationException if the types are not equal + */ + public static int checkNativeTypeEquals(int expectedNativeType, int actualNativeType) { + if (expectedNativeType != actualNativeType) { + throw new UnsupportedOperationException( + String.format("Expected native type %d, but got %d", + expectedNativeType, actualNativeType)); + } + + return actualNativeType; + } + + private MarshalHelpers() { + throw new AssertionError(); + } +} diff --git a/core/java/android/hardware/camera2/marshal/MarshalQueryable.java b/core/java/android/hardware/camera2/marshal/MarshalQueryable.java new file mode 100644 index 0000000..35fed1f --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/MarshalQueryable.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 android.hardware.camera2.marshal; + +import android.hardware.camera2.utils.TypeReference; + +/** + * Query if a marshaler can marshal to/from a particular native and managed type; if it supports + * the combination, allow creating a marshaler instance to do the serialization. + * + * <p>Not all queryable instances will support exactly one combination. Some, such as the + * primitive queryable will support all primitive to/from managed mappings (as long as they are + * 1:1). Others, such as the rectangle queryable will only support integer to rectangle mappings. + * </p> + * + * <p>Yet some others are codependent on other queryables; e.g. array queryables might only support + * a type map for {@code T[]} if another queryable exists with support for the component type + * {@code T}.</p> + */ +public interface MarshalQueryable<T> { + /** + * Create a marshaler between the selected managed and native type. + * + * <p>This marshaler instance is only good for that specific type mapping; and will refuse + * to map other managed types, other native types, or an other combination that isn't + * this exact one.</p> + * + * @param managedType a managed type reference + * @param nativeType the native type, e.g. + * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE} + * @return + * + * @throws UnsupportedOperationException + * if {@link #isTypeMappingSupported} returns {@code false} + */ + public Marshaler<T> createMarshaler( + TypeReference<T> managedType, int nativeType); + + /** + * Determine whether or not this query marshal is able to create a marshaler that will + * support the managed type and native type mapping. + * + * <p>If this returns {@code true}, then a marshaler can be instantiated by + * {@link #createMarshaler} that will marshal data to/from the native type + * from/to the managed type.</p> + * + * <p>Most marshalers are likely to only support one type map.</p> + */ + public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType); +} diff --git a/core/java/android/hardware/camera2/marshal/MarshalRegistry.java b/core/java/android/hardware/camera2/marshal/MarshalRegistry.java new file mode 100644 index 0000000..92d9057 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/MarshalRegistry.java @@ -0,0 +1,133 @@ +/* + * 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.marshal; + +import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.utils.TypeReference; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Registry of supported marshalers; add new query-able marshalers or lookup existing ones.</p> + */ +public class MarshalRegistry { + + /** + * Register a marshal queryable for the managed type {@code T}. + * + * <p>Multiple marshal queryables for the same managed type {@code T} may be registered; + * this is desirable if they support different native types (e.g. marshaler 1 supports + * {@code Integer <-> TYPE_INT32}, marshaler 2 supports {@code Integer <-> TYPE_BYTE}.</p> + * + * @param queryable a non-{@code null} marshal queryable that supports marshaling {@code T} + */ + public static <T> void registerMarshalQueryable(MarshalQueryable<T> queryable) { + sRegisteredMarshalQueryables.add(queryable); + } + + /** + * Lookup a marshaler between {@code T} and {@code nativeType}. + * + * <p>Marshalers are looked up in the order they were registered; earlier registered + * marshal queriers get priority.</p> + * + * @param typeToken The compile-time type reference for {@code T} + * @param nativeType The native type, e.g. {@link CameraMetadataNative#TYPE_BYTE TYPE_BYTE} + * @return marshaler a non-{@code null} marshaler that supports marshaling the type combo + * + * @throws UnsupportedOperationException If no marshaler matching the args could be found + */ + @SuppressWarnings("unchecked") + public static <T> Marshaler<T> getMarshaler(TypeReference<T> typeToken, int nativeType) { + // TODO: can avoid making a new token each time by code-genning + // the list of type tokens and native types from the keys (at the call sites) + MarshalToken<T> marshalToken = new MarshalToken<T>(typeToken, nativeType); + + /* + * Marshalers are instantiated lazily once they are looked up; successive lookups + * will not instantiate new marshalers. + */ + Marshaler<T> marshaler = + (Marshaler<T>) sMarshalerMap.get(marshalToken); + + if (sRegisteredMarshalQueryables.size() == 0) { + throw new AssertionError("No available query marshalers registered"); + } + + if (marshaler == null) { + // Query each marshaler to see if they support the native/managed type combination + for (MarshalQueryable<?> potentialMarshaler : sRegisteredMarshalQueryables) { + + MarshalQueryable<T> castedPotential = + (MarshalQueryable<T>)potentialMarshaler; + + if (castedPotential.isTypeMappingSupported(typeToken, nativeType)) { + marshaler = castedPotential.createMarshaler(typeToken, nativeType); + break; + } + } + } + + if (marshaler == null) { + throw new UnsupportedOperationException( + "Could not find marshaler that matches the requested " + + "combination of type reference " + + typeToken + " and native type " + + MarshalHelpers.toStringNativeType(nativeType)); + } + + sMarshalerMap.put(marshalToken, marshaler); + + return marshaler; + } + + private static class MarshalToken<T> { + public MarshalToken(TypeReference<T> typeReference, int nativeType) { + this.typeReference = typeReference; + this.nativeType = nativeType; + } + + final TypeReference<T> typeReference; + final int nativeType; + + @Override + public boolean equals(Object other) { + if (other instanceof MarshalToken<?>) { + MarshalToken<?> otherToken = (MarshalToken<?>)other; + return typeReference.equals(otherToken.typeReference) && + nativeType == otherToken.nativeType; + } + + return false; + } + + @Override + public int hashCode() { + return typeReference.hashCode() ^ nativeType; + } + } + + private static List<MarshalQueryable<?>> sRegisteredMarshalQueryables = + new ArrayList<MarshalQueryable<?>>(); + private static HashMap<MarshalToken<?>, Marshaler<?>> sMarshalerMap = + new HashMap<MarshalToken<?>, Marshaler<?>>(); + + private MarshalRegistry() { + throw new AssertionError(); + } +} diff --git a/core/java/android/hardware/camera2/marshal/Marshaler.java b/core/java/android/hardware/camera2/marshal/Marshaler.java new file mode 100644 index 0000000..eb0ad15 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/Marshaler.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.hardware.camera2.marshal; + +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +import static android.hardware.camera2.marshal.MarshalHelpers.*; +import static com.android.internal.util.Preconditions.*; + +/** + * Base class to marshal data to/from managed/native metadata byte buffers. + * + * <p>This class should not be created directly; an instance of it can be obtained + * using {@link MarshalQueryable#createMarshaler} for the same type {@code T} if the native type + * mapping for {@code T} {@link MarshalQueryable#isTypeMappingSupported supported}.</p> + * + * @param <T> the compile-time managed type + */ +public abstract class Marshaler<T> { + + protected final TypeReference<T> mTypeReference; + protected final int mNativeType; + + /** + * Instantiate a marshaler between a single managed/native type combination. + * + * <p>This particular managed/native type combination must be supported by + * {@link #isTypeMappingSupported}.</p> + * + * @param query an instance of {@link MarshalQueryable} + * @param typeReference the managed type reference + * Must be one for which {@link #isTypeMappingSupported} returns {@code true} + * @param nativeType the native type, e.g. + * {@link android.hardware.camera2.impl.CameraMetadataNative#TYPE_BYTE TYPE_BYTE}. + * Must be one for which {@link #isTypeMappingSupported} returns {@code true}. + * + * @throws NullPointerException if any args were {@code null} + * @throws UnsupportedOperationException if the type mapping was not supported + */ + protected Marshaler( + MarshalQueryable<T> query, TypeReference<T> typeReference, int nativeType) { + mTypeReference = checkNotNull(typeReference, "typeReference must not be null"); + mNativeType = checkNativeType(nativeType); + + if (!query.isTypeMappingSupported(typeReference, nativeType)) { + throw new UnsupportedOperationException( + "Unsupported type marshaling for managed type " + + typeReference + " and native type " + + MarshalHelpers.toStringNativeType(nativeType)); + } + } + + /** + * Marshal the specified object instance (value) into a byte buffer. + * + * <p>Upon completion, the {@link ByteBuffer#position()} will have advanced by + * the {@link #calculateMarshalSize marshal size} of {@code value}.</p> + * + * @param value the value of type T that we wish to write into the byte buffer + * @param buffer the byte buffer into which the marshaled object will be written + */ + public abstract void marshal(T value, ByteBuffer buffer); + + /** + * Get the size in bytes for how much space would be required to write this {@code value} + * into a byte buffer using the given {@code nativeType}. + * + * <p>If the size of this {@code T} instance when serialized into a buffer is always constant, + * then this method will always return the same value (and particularly, it will return + * an equivalent value to {@link #getNativeSize()}.</p> + * + * <p>Overriding this method is a must when the size is {@link NATIVE_SIZE_DYNAMIC dynamic}.</p> + * + * @param value the value of type T that we wish to write into the byte buffer + * @return the size that would need to be written to the byte buffer + */ + public int calculateMarshalSize(T value) { + int nativeSize = getNativeSize(); + + if (nativeSize == NATIVE_SIZE_DYNAMIC) { + throw new AssertionError("Override this function for dynamically-sized objects"); + } + + return nativeSize; + } + + /** + * Unmarshal a new object instance from the byte buffer into its managed type. + * + * <p>Upon completion, the {@link ByteBuffer#position()} will have advanced by + * the {@link #calculateMarshalSize marshal size} of the returned {@code T} instance.</p> + * + * @param buffer the byte buffer, from which we will read the object + * @return a new instance of type T read from the byte buffer + */ + public abstract T unmarshal(ByteBuffer buffer); + + /** + * Used to denote variable-length data structures. + * + * <p>If the size is dynamic then we can't know ahead of time how big of a data structure + * to preallocate for e.g. arrays, so one object must be unmarshaled at a time.</p> + */ + public static int NATIVE_SIZE_DYNAMIC = -1; + + /** + * How many bytes a single instance of {@code T} will take up if marshalled to/from + * {@code nativeType}. + * + * <p>When unmarshaling data from native to managed, the instance {@code T} is not yet + * available. If the native size is always a fixed mapping regardless of the instance of + * {@code T} (e.g. if the type is not a container of some sort), it can be used to preallocate + * containers for {@code T} to avoid resizing them.</p> + * + * <p>In particular, the array marshaler takes advantage of this (when size is not dynamic) + * to preallocate arrays of the right length when unmarshaling an array {@code T[]}.</p> + * + * @return a size in bytes, or {@link #NATIVE_SIZE_DYNAMIC} if the size is dynamic + */ + public abstract int getNativeSize(); + + /** + * The type reference for {@code T} for the managed type side of this marshaler. + */ + public TypeReference<T> getTypeReference() { + return mTypeReference; + } + + /** The native type corresponding to this marshaler for the native side of this marshaler.*/ + public int getNativeType() { + return mNativeType; + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java new file mode 100644 index 0000000..22b87ef --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableArray.java @@ -0,0 +1,182 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.marshal.MarshalRegistry; +import android.hardware.camera2.utils.TypeReference; +import android.util.Log; + +import java.lang.reflect.Array; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal any array {@code T}. + * + * <p>To marshal any {@code T} to/from a native type, the marshaler for T to/from that native type + * also has to exist.</p> + * + * <p>{@code T} can be either a T2[] where T2 is an object type, or a P[] where P is a + * built-in primitive (e.g. int[], float[], etc).</p> + + * @param <T> the type of the array (e.g. T = int[], or T = Rational[]) + */ +public class MarshalQueryableArray<T> implements MarshalQueryable<T> { + + private static final String TAG = MarshalQueryableArray.class.getSimpleName(); + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private class MarshalerArray extends Marshaler<T> { + private final Class<T> mClass; + private final Marshaler<?> mComponentMarshaler; + private final Class<?> mComponentClass; + + @SuppressWarnings("unchecked") + protected MarshalerArray(TypeReference<T> typeReference, int nativeType) { + super(MarshalQueryableArray.this, typeReference, nativeType); + + mClass = (Class<T>)typeReference.getRawType(); + + TypeReference<?> componentToken = typeReference.getComponentType(); + mComponentMarshaler = MarshalRegistry.getMarshaler(componentToken, mNativeType); + mComponentClass = componentToken.getRawType(); + } + + @Override + public void marshal(T value, ByteBuffer buffer) { + int length = Array.getLength(value); + for (int i = 0; i < length; ++i) { + marshalArrayElement(mComponentMarshaler, buffer, value, i); + } + } + + @Override + public T unmarshal(ByteBuffer buffer) { + Object array; + + int elementSize = mComponentMarshaler.getNativeSize(); + + if (elementSize != Marshaler.NATIVE_SIZE_DYNAMIC) { + int remaining = buffer.remaining(); + int arraySize = remaining / elementSize; + + if (remaining % elementSize != 0) { + throw new UnsupportedOperationException("Arrays for " + mTypeReference + + " must be packed tighly into a multiple of " + elementSize + + "; but there are " + (remaining % elementSize) + " left over bytes"); + } + + if (VERBOSE) { + Log.v(TAG, String.format( + "Attempting to unpack array (count = %d, element size = %d, bytes " + + "remaining = %d) for type %s", + arraySize, elementSize, remaining, mClass)); + } + + array = Array.newInstance(mComponentClass, arraySize); + for (int i = 0; i < arraySize; ++i) { + Object elem = mComponentMarshaler.unmarshal(buffer); + Array.set(array, i, elem); + } + } else { + // Dynamic size, use an array list. + ArrayList<Object> arrayList = new ArrayList<Object>(); + + // Assumes array is packed tightly; no unused bytes allowed + while (buffer.hasRemaining()) { + Object elem = mComponentMarshaler.unmarshal(buffer); + arrayList.add(elem); + } + + int arraySize = arrayList.size(); + array = copyListToArray(arrayList, Array.newInstance(mComponentClass, arraySize)); + } + + if (buffer.remaining() != 0) { + Log.e(TAG, "Trailing bytes (" + buffer.remaining() + ") left over after unpacking " + + mClass); + } + + return mClass.cast(array); + } + + @Override + public int getNativeSize() { + return NATIVE_SIZE_DYNAMIC; + } + + @Override + public int calculateMarshalSize(T value) { + int elementSize = mComponentMarshaler.getNativeSize(); + int arrayLength = Array.getLength(value); + + if (elementSize != Marshaler.NATIVE_SIZE_DYNAMIC) { + // The fast way. Every element size is uniform. + return elementSize * arrayLength; + } else { + // The slow way. Accumulate size for each element. + int size = 0; + for (int i = 0; i < arrayLength; ++i) { + size += calculateElementMarshalSize(mComponentMarshaler, value, i); + } + + return size; + } + } + + /* + * Helpers to avoid compiler errors regarding types with wildcards (?) + */ + + @SuppressWarnings("unchecked") + private <TElem> void marshalArrayElement(Marshaler<TElem> marshaler, + ByteBuffer buffer, Object array, int index) { + marshaler.marshal((TElem)Array.get(array, index), buffer); + } + + @SuppressWarnings("unchecked") + private Object copyListToArray(ArrayList<?> arrayList, Object arrayDest) { + return arrayList.toArray((T[]) arrayDest); + } + + @SuppressWarnings("unchecked") + private <TElem> int calculateElementMarshalSize(Marshaler<TElem> marshaler, + Object array, int index) { + Object elem = Array.get(array, index); + + return marshaler.calculateMarshalSize((TElem) elem); + } + } + + @Override + public Marshaler<T> createMarshaler(TypeReference<T> managedType, int nativeType) { + return new MarshalerArray(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) { + // support both ConcreteType[] and GenericType<ConcreteType>[] + return managedType.getRawType().isArray(); + + // TODO: Should this recurse deeper and check that there is + // a valid marshaler for the ConcreteType as well? + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableBoolean.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableBoolean.java new file mode 100644 index 0000000..4aa4b4a --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableBoolean.java @@ -0,0 +1,67 @@ +/* + * 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.marshal.impl; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +/** + * Marshal booleans: TYPE_BYTE <-> boolean/Boolean + */ +public class MarshalQueryableBoolean implements MarshalQueryable<Boolean> { + + private class MarshalerBoolean extends Marshaler<Boolean> { + protected MarshalerBoolean(TypeReference<Boolean> typeReference, int nativeType) { + super(MarshalQueryableBoolean.this, typeReference, nativeType); + } + + @Override + public void marshal(Boolean value, ByteBuffer buffer) { + boolean unboxValue = value; + buffer.put((byte)(unboxValue ? 1 : 0)); + } + + @Override + public Boolean unmarshal(ByteBuffer buffer) { + return buffer.get() != 0; + } + + @Override + public int getNativeSize() { + return SIZEOF_BYTE; + } + } + + @Override + public Marshaler<Boolean> createMarshaler(TypeReference<Boolean> managedType, + int nativeType) { + return new MarshalerBoolean(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Boolean> managedType, int nativeType) { + return (Boolean.class.equals(managedType.getType()) + || boolean.class.equals(managedType.getType())) && nativeType == TYPE_BYTE; + } + + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java new file mode 100644 index 0000000..47f79bf --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.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 android.hardware.camera2.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.ColorSpaceTransform; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal {@link ColorSpaceTransform} to/from {@link #TYPE_RATIONAL} + */ +public class MarshalQueryableColorSpaceTransform implements + MarshalQueryable<ColorSpaceTransform> { + + private static final int ELEMENTS_INT32 = 3 * 3 * (SIZEOF_RATIONAL / SIZEOF_INT32); + private static final int SIZE = SIZEOF_INT32 * ELEMENTS_INT32; + + /** rational x 3 x 3 */ + private class MarshalerColorSpaceTransform extends Marshaler<ColorSpaceTransform> { + protected MarshalerColorSpaceTransform(TypeReference<ColorSpaceTransform> typeReference, + int nativeType) { + super(MarshalQueryableColorSpaceTransform.this, typeReference, nativeType); + } + + @Override + public void marshal(ColorSpaceTransform value, ByteBuffer buffer) { + int[] transformAsArray = new int[ELEMENTS_INT32]; + value.copyElements(transformAsArray, /*offset*/0); + + for (int i = 0; i < ELEMENTS_INT32; ++i) { + buffer.putInt(transformAsArray[i]); + } + } + + @Override + public ColorSpaceTransform unmarshal(ByteBuffer buffer) { + int[] transformAsArray = new int[ELEMENTS_INT32]; + + for (int i = 0; i < ELEMENTS_INT32; ++i) { + transformAsArray[i] = buffer.getInt(); + } + + return new ColorSpaceTransform(transformAsArray); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<ColorSpaceTransform> createMarshaler( + TypeReference<ColorSpaceTransform> managedType, int nativeType) { + return new MarshalerColorSpaceTransform(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported( + TypeReference<ColorSpaceTransform> managedType, int nativeType) { + return nativeType == TYPE_RATIONAL && + ColorSpaceTransform.class.equals(managedType.getType()); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java new file mode 100644 index 0000000..fa53db2 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableEnum.java @@ -0,0 +1,220 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.util.HashMap; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal any simple enum (0-arg constructors only) into/from either + * {@code TYPE_BYTE} or {@code TYPE_INT32}. + * + * <p>Default values of the enum are mapped to its ordinal; this can be overridden + * by providing a manual value with {@link #registerEnumValues}.</p> + + * @param <T> the type of {@code Enum} + */ +public class MarshalQueryableEnum<T extends Enum<T>> implements MarshalQueryable<T> { + + private static final String TAG = MarshalQueryableEnum.class.getSimpleName(); + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private static final int UINT8_MIN = 0x0; + private static final int UINT8_MAX = (1 << Byte.SIZE) - 1; + private static final int UINT8_MASK = UINT8_MAX; + + private class MarshalerEnum extends Marshaler<T> { + + private final Class<T> mClass; + + @SuppressWarnings("unchecked") + protected MarshalerEnum(TypeReference<T> typeReference, int nativeType) { + super(MarshalQueryableEnum.this, typeReference, nativeType); + + mClass = (Class<T>)typeReference.getRawType(); + } + + @Override + public void marshal(T value, ByteBuffer buffer) { + int enumValue = getEnumValue(value); + + if (mNativeType == TYPE_INT32) { + buffer.putInt(enumValue); + } else if (mNativeType == TYPE_BYTE) { + if (enumValue < UINT8_MIN || enumValue > UINT8_MAX) { + throw new UnsupportedOperationException(String.format( + "Enum value %x too large to fit into unsigned byte", enumValue)); + } + buffer.put((byte)enumValue); + } else { + throw new AssertionError(); + } + } + + @Override + public T unmarshal(ByteBuffer buffer) { + int enumValue; + + switch (mNativeType) { + case TYPE_INT32: + enumValue = buffer.getInt(); + break; + case TYPE_BYTE: + // get the unsigned byte value; avoid sign extension + enumValue = buffer.get() & UINT8_MASK; + break; + default: + throw new AssertionError( + "Unexpected native type; impossible since its not supported"); + } + + return getEnumFromValue(mClass, enumValue); + } + + @Override + public int getNativeSize() { + return getPrimitiveTypeSize(mNativeType); + } + } + + @Override + public Marshaler<T> createMarshaler(TypeReference<T> managedType, int nativeType) { + return new MarshalerEnum(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) { + if (nativeType == TYPE_INT32 || nativeType == TYPE_BYTE) { + if (managedType.getType() instanceof Class<?>) { + Class<?> typeClass = (Class<?>)managedType.getType(); + + if (typeClass.isEnum()) { + if (VERBOSE) { + Log.v(TAG, "possible enum detected for " + typeClass); + } + + // The enum must not take extra arguments + try { + // match a class like: "public enum Fruits { Apple, Orange; }" + typeClass.getDeclaredConstructor(String.class, int.class); + return true; + } catch (NoSuchMethodException e) { + // Skip: custom enum with a special constructor e.g. Foo(T), but need Foo() + Log.e(TAG, "Can't marshal class " + typeClass + "; no default constructor"); + } catch (SecurityException e) { + // Skip: wouldn't be able to touch the enum anyway + Log.e(TAG, "Can't marshal class " + typeClass + "; not accessible"); + } + } + } + } + + return false; + } + + @SuppressWarnings("rawtypes") + private static final HashMap<Class<? extends Enum>, int[]> sEnumValues = + new HashMap<Class<? extends Enum>, int[]>(); + + /** + * Register a non-sequential set of values to be used with the marshal/unmarshal functions. + * + * <p>This enables get/set to correctly marshal the enum into a value that is C-compatible.</p> + * + * @param enumType The class for an enum + * @param values A list of values mapping to the ordinals of the enum + */ + public static <T extends Enum<T>> void registerEnumValues(Class<T> enumType, int[] values) { + if (enumType.getEnumConstants().length != values.length) { + throw new IllegalArgumentException( + "Expected values array to be the same size as the enumTypes values " + + values.length + " for type " + enumType); + } + if (VERBOSE) { + Log.v(TAG, "Registered enum values for type " + enumType + " values"); + } + + sEnumValues.put(enumType, values); + } + + /** + * Get the numeric value from an enum. + * + * <p>This is usually the same as the ordinal value for + * enums that have fully sequential values, although for C-style enums the range of values + * may not map 1:1.</p> + * + * @param enumValue Enum instance + * @return Int guaranteed to be ABI-compatible with the C enum equivalent + */ + private static <T extends Enum<T>> int getEnumValue(T enumValue) { + int[] values; + values = sEnumValues.get(enumValue.getClass()); + + int ordinal = enumValue.ordinal(); + if (values != null) { + return values[ordinal]; + } + + return ordinal; + } + + /** + * Finds the enum corresponding to it's numeric value. Opposite of {@link #getEnumValue} method. + * + * @param enumType Class of the enum we want to find + * @param value The numeric value of the enum + * @return An instance of the enum + */ + private static <T extends Enum<T>> T getEnumFromValue(Class<T> enumType, int value) { + int ordinal; + + int[] registeredValues = sEnumValues.get(enumType); + if (registeredValues != null) { + ordinal = -1; + + for (int i = 0; i < registeredValues.length; ++i) { + if (registeredValues[i] == value) { + ordinal = i; + break; + } + } + } else { + ordinal = value; + } + + T[] values = enumType.getEnumConstants(); + + if (ordinal < 0 || ordinal >= values.length) { + throw new IllegalArgumentException( + String.format( + "Argument 'value' (%d) was not a valid enum value for type %s " + + "(registered? %b)", + value, + enumType, (registeredValues != null))); + } + + return values[ordinal]; + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java new file mode 100644 index 0000000..01780db --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.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.hardware.camera2.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal {@link MeteringRectangle} to/from {@link #TYPE_INT32} + */ +public class MarshalQueryableMeteringRectangle implements MarshalQueryable<MeteringRectangle> { + private static final int SIZE = SIZEOF_INT32 * 5; + + /** (xmin, ymin, xmax, ymax, weight) */ + private class MarshalerMeteringRectangle extends Marshaler<MeteringRectangle> { + protected MarshalerMeteringRectangle(TypeReference<MeteringRectangle> typeReference, + int nativeType) { + super(MarshalQueryableMeteringRectangle.this, typeReference, nativeType); + } + + @Override + public void marshal(MeteringRectangle value, ByteBuffer buffer) { + int xMin = value.getX(); + int yMin = value.getY(); + int xMax = xMin + value.getWidth(); + int yMax = yMin + value.getHeight(); + int weight = value.getMeteringWeight(); + + buffer.putInt(xMin); + buffer.putInt(yMin); + buffer.putInt(xMax); + buffer.putInt(yMax); + buffer.putInt(weight); + } + + @Override + public MeteringRectangle unmarshal(ByteBuffer buffer) { + int xMin = buffer.getInt(); + int yMin = buffer.getInt(); + int xMax = buffer.getInt(); + int yMax = buffer.getInt(); + int weight = buffer.getInt(); + + int width = xMax - xMin; + int height = yMax - yMin; + + return new MeteringRectangle(xMin, yMin, width, height, weight); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<MeteringRectangle> createMarshaler( + TypeReference<MeteringRectangle> managedType, int nativeType) { + return new MarshalerMeteringRectangle(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported( + TypeReference<MeteringRectangle> managedType, int nativeType) { + return nativeType == TYPE_INT32 && MeteringRectangle.class.equals(managedType.getType()); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableNativeByteToInteger.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableNativeByteToInteger.java new file mode 100644 index 0000000..3b89c82 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableNativeByteToInteger.java @@ -0,0 +1,70 @@ +/* + * 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.marshal.impl; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +/** + * Marshal fake native enums (ints): TYPE_BYTE <-> int/Integer + */ +public class MarshalQueryableNativeByteToInteger implements MarshalQueryable<Integer> { + + private static final int UINT8_MASK = (1 << Byte.SIZE) - 1; + + private class MarshalerNativeByteToInteger extends Marshaler<Integer> { + protected MarshalerNativeByteToInteger(TypeReference<Integer> typeReference, + int nativeType) { + super(MarshalQueryableNativeByteToInteger.this, typeReference, nativeType); + } + + @Override + public void marshal(Integer value, ByteBuffer buffer) { + buffer.put((byte)(int)value); // truncate down to byte + } + + @Override + public Integer unmarshal(ByteBuffer buffer) { + // expand unsigned byte to int; avoid sign extension + return buffer.get() & UINT8_MASK; + } + + @Override + public int getNativeSize() { + return SIZEOF_BYTE; + } + } + + @Override + public Marshaler<Integer> createMarshaler(TypeReference<Integer> managedType, + int nativeType) { + return new MarshalerNativeByteToInteger(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Integer> managedType, int nativeType) { + return (Integer.class.equals(managedType.getType()) + || int.class.equals(managedType.getType())) && nativeType == TYPE_BYTE; + } + + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePair.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePair.java new file mode 100644 index 0000000..0a9935d --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePair.java @@ -0,0 +1,158 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.marshal.MarshalRegistry; +import android.hardware.camera2.utils.TypeReference; +import android.util.Pair; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; + +/** + * Marshal {@link Pair} to/from any native type + */ +public class MarshalQueryablePair<T1, T2> + implements MarshalQueryable<Pair<T1, T2>> { + + private class MarshalerPair extends Marshaler<Pair<T1, T2>> { + private final Class<? super Pair<T1, T2>> mClass; + private final Constructor<Pair<T1, T2>> mConstructor; + /** Marshal the {@code T1} inside of {@code Pair<T1, T2>} */ + private final Marshaler<T1> mNestedTypeMarshalerFirst; + /** Marshal the {@code T1} inside of {@code Pair<T1, T2>} */ + private final Marshaler<T2> mNestedTypeMarshalerSecond; + + @SuppressWarnings("unchecked") + protected MarshalerPair(TypeReference<Pair<T1, T2>> typeReference, + int nativeType) { + super(MarshalQueryablePair.this, typeReference, nativeType); + + mClass = typeReference.getRawType(); + + /* + * Lookup the actual type arguments, e.g. Pair<Integer, Float> --> [Integer, Float] + * and then get the marshalers for that managed type. + */ + ParameterizedType paramType; + try { + paramType = (ParameterizedType) typeReference.getType(); + } catch (ClassCastException e) { + throw new AssertionError("Raw use of Pair is not supported", e); + } + + // Get type marshaler for T1 + { + Type actualTypeArgument = paramType.getActualTypeArguments()[0]; + + TypeReference<?> actualTypeArgToken = + TypeReference.createSpecializedTypeReference(actualTypeArgument); + + mNestedTypeMarshalerFirst = (Marshaler<T1>)MarshalRegistry.getMarshaler( + actualTypeArgToken, mNativeType); + } + // Get type marshaler for T2 + { + Type actualTypeArgument = paramType.getActualTypeArguments()[1]; + + TypeReference<?> actualTypeArgToken = + TypeReference.createSpecializedTypeReference(actualTypeArgument); + + mNestedTypeMarshalerSecond = (Marshaler<T2>)MarshalRegistry.getMarshaler( + actualTypeArgToken, mNativeType); + } + try { + mConstructor = (Constructor<Pair<T1, T2>>)mClass.getConstructor( + Object.class, Object.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + @Override + public void marshal(Pair<T1, T2> value, ByteBuffer buffer) { + if (value.first == null) { + throw new UnsupportedOperationException("Pair#first must not be null"); + } else if (value.second == null) { + throw new UnsupportedOperationException("Pair#second must not be null"); + } + + mNestedTypeMarshalerFirst.marshal(value.first, buffer); + mNestedTypeMarshalerSecond.marshal(value.second, buffer); + } + + @Override + public Pair<T1, T2> unmarshal(ByteBuffer buffer) { + T1 first = mNestedTypeMarshalerFirst.unmarshal(buffer); + T2 second = mNestedTypeMarshalerSecond.unmarshal(buffer); + + try { + return mConstructor.newInstance(first, second); + } catch (InstantiationException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (IllegalArgumentException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new AssertionError(e); + } + } + + @Override + public int getNativeSize() { + int firstSize = mNestedTypeMarshalerFirst.getNativeSize(); + int secondSize = mNestedTypeMarshalerSecond.getNativeSize(); + + if (firstSize != NATIVE_SIZE_DYNAMIC && secondSize != NATIVE_SIZE_DYNAMIC) { + return firstSize + secondSize; + } else { + return NATIVE_SIZE_DYNAMIC; + } + } + + @Override + public int calculateMarshalSize(Pair<T1, T2> value) { + int nativeSize = getNativeSize(); + + if (nativeSize != NATIVE_SIZE_DYNAMIC) { + return nativeSize; + } else { + int firstSize = mNestedTypeMarshalerFirst.calculateMarshalSize(value.first); + int secondSize = mNestedTypeMarshalerSecond.calculateMarshalSize(value.second); + + return firstSize + secondSize; + } + } + } + + @Override + public Marshaler<Pair<T1, T2>> createMarshaler(TypeReference<Pair<T1, T2>> managedType, + int nativeType) { + return new MarshalerPair(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Pair<T1, T2>> managedType, int nativeType) { + return (Pair.class.equals(managedType.getRawType())); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.java new file mode 100644 index 0000000..1fd6a1d --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableParcelable.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 android.hardware.camera2.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal any {@code T extends Parcelable} to/from any native type + * + * <p>Use with extreme caution! File descriptors and binders will not be marshaled across.</p> + */ +public class MarshalQueryableParcelable<T extends Parcelable> + implements MarshalQueryable<T> { + + private static final String TAG = "MarshalParcelable"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private static final String FIELD_CREATOR = "CREATOR"; + + private class MarshalerParcelable extends Marshaler<T> { + + private final Class<T> mClass; + private final Parcelable.Creator<T> mCreator; + + @SuppressWarnings("unchecked") + protected MarshalerParcelable(TypeReference<T> typeReference, + int nativeType) { + super(MarshalQueryableParcelable.this, typeReference, nativeType); + + mClass = (Class<T>)typeReference.getRawType(); + Field creatorField; + try { + creatorField = mClass.getDeclaredField(FIELD_CREATOR); + } catch (NoSuchFieldException e) { + // Impossible. All Parcelable implementations must have a 'CREATOR' static field + throw new AssertionError(e); + } + + try { + mCreator = (Parcelable.Creator<T>)creatorField.get(null); + } catch (IllegalAccessException e) { + // Impossible: All 'CREATOR' static fields must be public + throw new AssertionError(e); + } catch (IllegalArgumentException e) { + // Impossible: This is a static field, so null must be ok + throw new AssertionError(e); + } + } + + @Override + public void marshal(T value, ByteBuffer buffer) { + if (VERBOSE) { + Log.v(TAG, "marshal " + value); + } + + Parcel parcel = Parcel.obtain(); + byte[] parcelContents; + + try { + value.writeToParcel(parcel, /*flags*/0); + + if (parcel.hasFileDescriptors()) { + throw new UnsupportedOperationException( + "Parcelable " + value + " must not have file descriptors"); + } + + parcelContents = parcel.marshall(); + } + finally { + parcel.recycle(); + } + + if (parcelContents.length == 0) { + throw new AssertionError("No data marshaled for " + value); + } + + buffer.put(parcelContents); + } + + @Override + public T unmarshal(ByteBuffer buffer) { + if (VERBOSE) { + Log.v(TAG, "unmarshal, buffer remaining " + buffer.remaining()); + } + + /* + * Quadratically slow when marshaling an array of parcelables. + * + * Read out the entire byte buffer as an array, then copy it into the parcel. + * + * Once we unparcel the entire object, advance the byte buffer by only how many + * bytes the parcel actually used up. + * + * Future: If we ever do need to use parcelable arrays, we can do this a little smarter + * by reading out a chunk like 4,8,16,24 each time, but not sure how to detect + * parcels being too short in this case. + * + * Future: Alternatively use Parcel#obtain(long) directly into the native + * pointer of a ByteBuffer, which would not copy if the ByteBuffer was direct. + */ + buffer.mark(); + + Parcel parcel = Parcel.obtain(); + try { + int maxLength = buffer.remaining(); + + byte[] remaining = new byte[maxLength]; + buffer.get(remaining); + + parcel.unmarshall(remaining, /*offset*/0, maxLength); + parcel.setDataPosition(/*pos*/0); + + T value = mCreator.createFromParcel(parcel); + int actualLength = parcel.dataPosition(); + + if (actualLength == 0) { + throw new AssertionError("No data marshaled for " + value); + } + + // set the position past the bytes the parcelable actually used + buffer.reset(); + buffer.position(buffer.position() + actualLength); + + if (VERBOSE) { + Log.v(TAG, "unmarshal, parcel length was " + actualLength); + Log.v(TAG, "unmarshal, value is " + value); + } + + return mClass.cast(value); + } finally { + parcel.recycle(); + } + } + + @Override + public int getNativeSize() { + return NATIVE_SIZE_DYNAMIC; + } + + @Override + public int calculateMarshalSize(T value) { + Parcel parcel = Parcel.obtain(); + try { + value.writeToParcel(parcel, /*flags*/0); + int length = parcel.marshall().length; + + if (VERBOSE) { + Log.v(TAG, "calculateMarshalSize, length when parceling " + + value + " is " + length); + } + + return length; + } finally { + parcel.recycle(); + } + } + } + + @Override + public Marshaler<T> createMarshaler(TypeReference<T> managedType, int nativeType) { + return new MarshalerParcelable(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) { + return Parcelable.class.isAssignableFrom(managedType.getRawType()); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java new file mode 100644 index 0000000..189b597 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java @@ -0,0 +1,185 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.impl.CameraMetadataNative; +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; +import android.util.Rational; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; +import static com.android.internal.util.Preconditions.*; + +import java.nio.ByteBuffer; + +/** + * Marshal/unmarshal built-in primitive types to and from a {@link ByteBuffer}. + * + * <p>The following list of type marshaling is supported: + * <ul> + * <li>byte <-> TYPE_BYTE + * <li>int <-> TYPE_INT32 + * <li>long <-> TYPE_INT64 + * <li>float <-> TYPE_FLOAT + * <li>double <-> TYPE_DOUBLE + * <li>Rational <-> TYPE_RATIONAL + * </ul> + * </p> + * + * <p>Due to the nature of generics, values are always boxed; this also means that both + * the boxed and unboxed types are supported (i.e. both {@code int} and {@code Integer}).</p> + * + * <p>Each managed type <!--(other than boolean)--> must correspond 1:1 to the native type + * (e.g. a byte will not map to a {@link CameraMetadataNative#TYPE_INT32 TYPE_INT32} or vice versa) + * for marshaling.</p> + */ +public final class MarshalQueryablePrimitive<T> implements MarshalQueryable<T> { + + private class MarshalerPrimitive extends Marshaler<T> { + /** Always the wrapped class variant of the primitive class for {@code T} */ + private final Class<T> mClass; + + @SuppressWarnings("unchecked") + protected MarshalerPrimitive(TypeReference<T> typeReference, int nativeType) { + super(MarshalQueryablePrimitive.this, typeReference, nativeType); + + // Turn primitives into wrappers, otherwise int.class.cast(Integer) will fail + mClass = wrapClassIfPrimitive((Class<T>)typeReference.getRawType()); + } + + @Override + public T unmarshal(ByteBuffer buffer) { + return mClass.cast(unmarshalObject(buffer)); + } + + @Override + public int calculateMarshalSize(T value) { + return getPrimitiveTypeSize(mNativeType); + } + + @Override + public void marshal(T value, ByteBuffer buffer) { + if (value instanceof Integer) { + checkNativeTypeEquals(TYPE_INT32, mNativeType); + final int val = (Integer) value; + marshalPrimitive(val, buffer); + } else if (value instanceof Float) { + checkNativeTypeEquals(TYPE_FLOAT, mNativeType); + final float val = (Float) value; + marshalPrimitive(val, buffer); + } else if (value instanceof Long) { + checkNativeTypeEquals(TYPE_INT64, mNativeType); + final long val = (Long) value; + marshalPrimitive(val, buffer); + } else if (value instanceof Rational) { + checkNativeTypeEquals(TYPE_RATIONAL, mNativeType); + marshalPrimitive((Rational) value, buffer); + } else if (value instanceof Double) { + checkNativeTypeEquals(TYPE_DOUBLE, mNativeType); + final double val = (Double) value; + marshalPrimitive(val, buffer); + } else if (value instanceof Byte) { + checkNativeTypeEquals(TYPE_BYTE, mNativeType); + final byte val = (Byte) value; + marshalPrimitive(val, buffer); + } else { + throw new UnsupportedOperationException( + "Can't marshal managed type " + mTypeReference); + } + } + + private void marshalPrimitive(int value, ByteBuffer buffer) { + buffer.putInt(value); + } + + private void marshalPrimitive(float value, ByteBuffer buffer) { + buffer.putFloat(value); + } + + private void marshalPrimitive(double value, ByteBuffer buffer) { + buffer.putDouble(value); + } + + private void marshalPrimitive(long value, ByteBuffer buffer) { + buffer.putLong(value); + } + + private void marshalPrimitive(Rational value, ByteBuffer buffer) { + buffer.putInt(value.getNumerator()); + buffer.putInt(value.getDenominator()); + } + + private void marshalPrimitive(byte value, ByteBuffer buffer) { + buffer.put(value); + } + + private Object unmarshalObject(ByteBuffer buffer) { + switch (mNativeType) { + case TYPE_INT32: + return buffer.getInt(); + case TYPE_FLOAT: + return buffer.getFloat(); + case TYPE_INT64: + return buffer.getLong(); + case TYPE_RATIONAL: + int numerator = buffer.getInt(); + int denominator = buffer.getInt(); + return new Rational(numerator, denominator); + case TYPE_DOUBLE: + return buffer.getDouble(); + case TYPE_BYTE: + return buffer.get(); // getByte + default: + throw new UnsupportedOperationException( + "Can't unmarshal native type " + mNativeType); + } + } + + @Override + public int getNativeSize() { + return getPrimitiveTypeSize(mNativeType); + } + } + + @Override + public Marshaler<T> createMarshaler(TypeReference<T> managedType, int nativeType) { + return new MarshalerPrimitive(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<T> managedType, int nativeType) { + if (managedType.getType() instanceof Class<?>) { + Class<?> klass = (Class<?>)managedType.getType(); + + if (klass == byte.class || klass == Byte.class) { + return nativeType == TYPE_BYTE; + } else if (klass == int.class || klass == Integer.class) { + return nativeType == TYPE_INT32; + } else if (klass == float.class || klass == Float.class) { + return nativeType == TYPE_FLOAT; + } else if (klass == long.class || klass == Long.class) { + return nativeType == TYPE_INT64; + } else if (klass == double.class || klass == Double.class) { + return nativeType == TYPE_DOUBLE; + } else if (klass == Rational.class) { + return nativeType == TYPE_RATIONAL; + } + } + return false; + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java new file mode 100644 index 0000000..8512804 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRange.java @@ -0,0 +1,139 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.marshal.MarshalRegistry; +import android.hardware.camera2.utils.TypeReference; +import android.util.Range; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal {@link Range} to/from any native type + */ +public class MarshalQueryableRange<T extends Comparable<? super T>> + implements MarshalQueryable<Range<T>> { + private static final int RANGE_COUNT = 2; + + private class MarshalerRange extends Marshaler<Range<T>> { + private final Class<? super Range<T>> mClass; + private final Constructor<Range<T>> mConstructor; + /** Marshal the {@code T} inside of {@code Range<T>} */ + private final Marshaler<T> mNestedTypeMarshaler; + + @SuppressWarnings("unchecked") + protected MarshalerRange(TypeReference<Range<T>> typeReference, + int nativeType) { + super(MarshalQueryableRange.this, typeReference, nativeType); + + mClass = typeReference.getRawType(); + + /* + * Lookup the actual type argument, e.g. Range<Integer> --> Integer + * and then get the marshaler for that managed type. + */ + ParameterizedType paramType; + try { + paramType = (ParameterizedType) typeReference.getType(); + } catch (ClassCastException e) { + throw new AssertionError("Raw use of Range is not supported", e); + } + Type actualTypeArgument = paramType.getActualTypeArguments()[0]; + + TypeReference<?> actualTypeArgToken = + TypeReference.createSpecializedTypeReference(actualTypeArgument); + + mNestedTypeMarshaler = (Marshaler<T>)MarshalRegistry.getMarshaler( + actualTypeArgToken, mNativeType); + try { + mConstructor = (Constructor<Range<T>>)mClass.getConstructor( + Comparable.class, Comparable.class); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + + @Override + public void marshal(Range<T> value, ByteBuffer buffer) { + mNestedTypeMarshaler.marshal(value.getLower(), buffer); + mNestedTypeMarshaler.marshal(value.getUpper(), buffer); + } + + @Override + public Range<T> unmarshal(ByteBuffer buffer) { + T lower = mNestedTypeMarshaler.unmarshal(buffer); + T upper = mNestedTypeMarshaler.unmarshal(buffer); + + try { + return mConstructor.newInstance(lower, upper); + } catch (InstantiationException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (IllegalArgumentException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new AssertionError(e); + } + } + + @Override + public int getNativeSize() { + int nestedSize = mNestedTypeMarshaler.getNativeSize(); + + if (nestedSize != NATIVE_SIZE_DYNAMIC) { + return nestedSize * RANGE_COUNT; + } else { + return NATIVE_SIZE_DYNAMIC; + } + } + + @Override + public int calculateMarshalSize(Range<T> value) { + int nativeSize = getNativeSize(); + + if (nativeSize != NATIVE_SIZE_DYNAMIC) { + return nativeSize; + } else { + int lowerSize = mNestedTypeMarshaler.calculateMarshalSize(value.getLower()); + int upperSize = mNestedTypeMarshaler.calculateMarshalSize(value.getUpper()); + + return lowerSize + upperSize; + } + } + } + + @Override + public Marshaler<Range<T>> createMarshaler(TypeReference<Range<T>> managedType, + int nativeType) { + return new MarshalerRange(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Range<T>> managedType, int nativeType) { + return (Range.class.equals(managedType.getRawType())); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRect.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRect.java new file mode 100644 index 0000000..de20a1f --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRect.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.hardware.camera2.marshal.impl; + +import android.graphics.Rect; +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal {@link Rect} to/from {@link #TYPE_INT32} + */ +public class MarshalQueryableRect implements MarshalQueryable<Rect> { + private static final int SIZE = SIZEOF_INT32 * 4; + + private class MarshalerRect extends Marshaler<Rect> { + protected MarshalerRect(TypeReference<Rect> typeReference, + int nativeType) { + super(MarshalQueryableRect.this, typeReference, nativeType); + } + + @Override + public void marshal(Rect value, ByteBuffer buffer) { + buffer.putInt(value.left); + buffer.putInt(value.top); + buffer.putInt(value.width()); + buffer.putInt(value.height()); + } + + @Override + public Rect unmarshal(ByteBuffer buffer) { + int left = buffer.getInt(); + int top = buffer.getInt(); + int width = buffer.getInt(); + int height = buffer.getInt(); + + int right = left + width; + int bottom = top + height; + + return new Rect(left, top, right, bottom); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<Rect> createMarshaler(TypeReference<Rect> managedType, int nativeType) { + return new MarshalerRect(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Rect> managedType, int nativeType) { + return nativeType == TYPE_INT32 && (Rect.class.equals(managedType.getType())); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java new file mode 100644 index 0000000..98a7ad7 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java @@ -0,0 +1,131 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.ReprocessFormatsMap; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.hardware.camera2.utils.TypeReference; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +/** + * Marshaler for {@code android.scaler.availableInputOutputFormatsMap} custom class + * {@link ReprocessFormatsMap} + */ +public class MarshalQueryableReprocessFormatsMap + implements MarshalQueryable<ReprocessFormatsMap> { + + private class MarshalerReprocessFormatsMap extends Marshaler<ReprocessFormatsMap> { + protected MarshalerReprocessFormatsMap( + TypeReference<ReprocessFormatsMap> typeReference, int nativeType) { + super(MarshalQueryableReprocessFormatsMap.this, typeReference, nativeType); + } + + @Override + public void marshal(ReprocessFormatsMap value, ByteBuffer buffer) { + /* + * // writing (static example, DNG+ZSL) + * int32_t[] contents = { + * RAW_OPAQUE, 3, RAW16, YUV_420_888, BLOB, + * RAW16, 2, YUV_420_888, BLOB, + * ..., + * INPUT_FORMAT, OUTPUT_FORMAT_COUNT, [OUTPUT_0, OUTPUT_1, ..., OUTPUT_FORMAT_COUNT-1] + * }; + */ + int[] inputs = StreamConfigurationMap.imageFormatToInternal(value.getInputs()); + for (int input : inputs) { + // INPUT_FORMAT + buffer.putInt(input); + + int[] outputs = + StreamConfigurationMap.imageFormatToInternal(value.getOutputs(input)); + // OUTPUT_FORMAT_COUNT + buffer.putInt(outputs.length); + + // [OUTPUT_0, OUTPUT_1, ..., OUTPUT_FORMAT_COUNT-1] + for (int output : outputs) { + buffer.putInt(output); + } + } + } + + @Override + public ReprocessFormatsMap unmarshal(ByteBuffer buffer) { + int len = buffer.remaining() / SIZEOF_INT32; + if (buffer.remaining() % SIZEOF_INT32 != 0) { + throw new AssertionError("ReprocessFormatsMap was not TYPE_INT32"); + } + + int[] entries = new int[len]; + + IntBuffer intBuffer = buffer.asIntBuffer(); + intBuffer.get(entries); + + // TODO: consider moving rest of parsing code from ReprocessFormatsMap to here + + return new ReprocessFormatsMap(entries); + } + + @Override + public int getNativeSize() { + return NATIVE_SIZE_DYNAMIC; + } + + @Override + public int calculateMarshalSize(ReprocessFormatsMap value) { + /* + * // writing (static example, DNG+ZSL) + * int32_t[] contents = { + * RAW_OPAQUE, 3, RAW16, YUV_420_888, BLOB, + * RAW16, 2, YUV_420_888, BLOB, + * ..., + * INPUT_FORMAT, OUTPUT_FORMAT_COUNT, [OUTPUT_0, OUTPUT_1, ..., OUTPUT_FORMAT_COUNT-1] + * }; + */ + int length = 0; + + int[] inputs = value.getInputs(); + for (int input : inputs) { + + length += 1; // INPUT_FORMAT + length += 1; // OUTPUT_FORMAT_COUNT + + int[] outputs = value.getOutputs(input); + length += outputs.length; // [OUTPUT_0, OUTPUT_1, ..., OUTPUT_FORMAT_COUNT-1] + } + + return length * SIZEOF_INT32; + } + } + + @Override + public Marshaler<ReprocessFormatsMap> createMarshaler( + TypeReference<ReprocessFormatsMap> managedType, int nativeType) { + return new MarshalerReprocessFormatsMap(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<ReprocessFormatsMap> managedType, + int nativeType) { + return nativeType == TYPE_INT32 && managedType.getType().equals(ReprocessFormatsMap.class); + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java new file mode 100644 index 0000000..4253a0a --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java @@ -0,0 +1,75 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.RggbChannelVector; +import android.hardware.camera2.utils.TypeReference; + +import java.nio.ByteBuffer; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +/** + * Marshal {@link RggbChannelVector} to/from {@link #TYPE_FLOAT} {@code x 4} + */ +public class MarshalQueryableRggbChannelVector implements MarshalQueryable<RggbChannelVector> { + private static final int SIZE = SIZEOF_FLOAT * RggbChannelVector.COUNT; + + private class MarshalerRggbChannelVector extends Marshaler<RggbChannelVector> { + protected MarshalerRggbChannelVector(TypeReference<RggbChannelVector> typeReference, + int nativeType) { + super(MarshalQueryableRggbChannelVector.this, typeReference, nativeType); + } + + @Override + public void marshal(RggbChannelVector value, ByteBuffer buffer) { + for (int i = 0; i < RggbChannelVector.COUNT; ++i) { + buffer.putFloat(value.getComponent(i)); + } + } + + @Override + public RggbChannelVector unmarshal(ByteBuffer buffer) { + float red = buffer.getFloat(); + float gEven = buffer.getFloat(); + float gOdd = buffer.getFloat(); + float blue = buffer.getFloat(); + + return new RggbChannelVector(red, gEven, gOdd, blue); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<RggbChannelVector> createMarshaler( + TypeReference<RggbChannelVector> managedType, int nativeType) { + return new MarshalerRggbChannelVector(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported( + TypeReference<RggbChannelVector> managedType, int nativeType) { + return nativeType == TYPE_FLOAT && (RggbChannelVector.class.equals(managedType.getType())); + } + +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java new file mode 100644 index 0000000..721644e --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java @@ -0,0 +1,68 @@ +/* + * 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.marshal.impl; + +import android.util.Size; +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import java.nio.ByteBuffer; + +/** + * Marshal {@link Size} to/from {@code TYPE_INT32} + */ +public class MarshalQueryableSize implements MarshalQueryable<Size> { + private static final int SIZE = SIZEOF_INT32 * 2; + + private class MarshalerSize extends Marshaler<Size> { + protected MarshalerSize(TypeReference<Size> typeReference, int nativeType) { + super(MarshalQueryableSize.this, typeReference, nativeType); + } + + @Override + public void marshal(Size value, ByteBuffer buffer) { + buffer.putInt(value.getWidth()); + buffer.putInt(value.getHeight()); + } + + @Override + public Size unmarshal(ByteBuffer buffer) { + int width = buffer.getInt(); + int height = buffer.getInt(); + + return new Size(width, height); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<Size> createMarshaler(TypeReference<Size> managedType, int nativeType) { + return new MarshalerSize(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<Size> managedType, int nativeType) { + return nativeType == TYPE_INT32 && (Size.class.equals(managedType.getType())); + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSizeF.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSizeF.java new file mode 100644 index 0000000..b60a46d --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSizeF.java @@ -0,0 +1,72 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; +import android.util.SizeF; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import java.nio.ByteBuffer; + +/** + * Marshal {@link SizeF} to/from {@code TYPE_FLOAT} + */ +public class MarshalQueryableSizeF implements MarshalQueryable<SizeF> { + + private static final int SIZE = SIZEOF_FLOAT * 2; + + private class MarshalerSizeF extends Marshaler<SizeF> { + + protected MarshalerSizeF(TypeReference<SizeF> typeReference, int nativeType) { + super(MarshalQueryableSizeF.this, typeReference, nativeType); + } + + @Override + public void marshal(SizeF value, ByteBuffer buffer) { + buffer.putFloat(value.getWidth()); + buffer.putFloat(value.getHeight()); + } + + @Override + public SizeF unmarshal(ByteBuffer buffer) { + float width = buffer.getFloat(); + float height = buffer.getFloat(); + + return new SizeF(width, height); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<SizeF> createMarshaler( + TypeReference<SizeF> managedType, int nativeType) { + return new MarshalerSizeF(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<SizeF> managedType, int nativeType) { + return nativeType == TYPE_FLOAT && (SizeF.class.equals(managedType.getType())); + } +} + diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java new file mode 100644 index 0000000..62ace31 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java @@ -0,0 +1,80 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.StreamConfiguration; +import android.hardware.camera2.utils.TypeReference; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import java.nio.ByteBuffer; + +/** + * Marshaler for {@code android.scaler.availableStreamConfigurations} custom class + * {@link StreamConfiguration} + * + * <p>Data is stored as {@code (format, width, height, input?)} tuples (int32).</p> + */ +public class MarshalQueryableStreamConfiguration + implements MarshalQueryable<StreamConfiguration> { + private static final int SIZE = SIZEOF_INT32 * 4; + + private class MarshalerStreamConfiguration extends Marshaler<StreamConfiguration> { + protected MarshalerStreamConfiguration(TypeReference<StreamConfiguration> typeReference, + int nativeType) { + super(MarshalQueryableStreamConfiguration.this, typeReference, nativeType); + } + + @Override + public void marshal(StreamConfiguration value, ByteBuffer buffer) { + buffer.putInt(value.getFormat()); + buffer.putInt(value.getWidth()); + buffer.putInt(value.getHeight()); + buffer.putInt(value.isInput() ? 1 : 0); + } + + @Override + public StreamConfiguration unmarshal(ByteBuffer buffer) { + int format = buffer.getInt(); + int width = buffer.getInt(); + int height = buffer.getInt(); + boolean input = buffer.getInt() != 0; + + return new StreamConfiguration(format, width, height, input); + } + + @Override + public int getNativeSize() { + return SIZE; + } + + } + + @Override + public Marshaler<StreamConfiguration> createMarshaler( + TypeReference<StreamConfiguration> managedType, int nativeType) { + return new MarshalerStreamConfiguration(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<StreamConfiguration> managedType, + int nativeType) { + return nativeType == TYPE_INT32 && managedType.getType().equals(StreamConfiguration.class); + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java new file mode 100644 index 0000000..fd3dfac --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.params.StreamConfigurationDuration; +import android.hardware.camera2.utils.TypeReference; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; +import static android.hardware.camera2.marshal.MarshalHelpers.*; + +import java.nio.ByteBuffer; + +/** + * Marshaler for custom class {@link StreamConfigurationDuration} for min-frame and stall durations. + * + * <p> + * Data is stored as {@code (format, width, height, durationNs)} tuples (int64). + * </p> + */ +public class MarshalQueryableStreamConfigurationDuration + implements MarshalQueryable<StreamConfigurationDuration> { + + private static final int SIZE = SIZEOF_INT64 * 4; + /** + * Values and-ed with this will do an unsigned int to signed long conversion; + * in other words the sign bit from the int will not be extended. + * */ + private static final long MASK_UNSIGNED_INT = 0x00000000ffffffffL; + + private class MarshalerStreamConfigurationDuration + extends Marshaler<StreamConfigurationDuration> { + + protected MarshalerStreamConfigurationDuration( + TypeReference<StreamConfigurationDuration> typeReference, int nativeType) { + super(MarshalQueryableStreamConfigurationDuration.this, typeReference, nativeType); + } + + @Override + public void marshal(StreamConfigurationDuration value, ByteBuffer buffer) { + buffer.putLong(value.getFormat() & MASK_UNSIGNED_INT); // unsigned int -> long + buffer.putLong(value.getWidth()); + buffer.putLong(value.getHeight()); + buffer.putLong(value.getDuration()); + } + + @Override + public StreamConfigurationDuration unmarshal(ByteBuffer buffer) { + int format = (int)buffer.getLong(); + int width = (int)buffer.getLong(); + int height = (int)buffer.getLong(); + long durationNs = buffer.getLong(); + + return new StreamConfigurationDuration(format, width, height, durationNs); + } + + @Override + public int getNativeSize() { + return SIZE; + } + } + + @Override + public Marshaler<StreamConfigurationDuration> createMarshaler( + TypeReference<StreamConfigurationDuration> managedType, int nativeType) { + return new MarshalerStreamConfigurationDuration(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<StreamConfigurationDuration> managedType, + int nativeType) { + return nativeType == TYPE_INT64 && + (StreamConfigurationDuration.class.equals(managedType.getType())); + } + +}
\ No newline at end of file diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableString.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableString.java new file mode 100644 index 0000000..bf518bb --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableString.java @@ -0,0 +1,110 @@ +/* + * 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.marshal.impl; + +import android.hardware.camera2.marshal.Marshaler; +import android.hardware.camera2.marshal.MarshalQueryable; +import android.hardware.camera2.utils.TypeReference; +import android.util.Log; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import static android.hardware.camera2.impl.CameraMetadataNative.*; + +/** + * Marshal {@link String} to/from {@link #TYPE_BYTE}. + */ +public class MarshalQueryableString implements MarshalQueryable<String> { + + private static final String TAG = MarshalQueryableString.class.getSimpleName(); + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + private static final byte NUL = (byte)'\0'; // used as string terminator + + private class MarshalerString extends Marshaler<String> { + + protected MarshalerString(TypeReference<String> typeReference, int nativeType) { + super(MarshalQueryableString.this, typeReference, nativeType); + } + + @Override + public void marshal(String value, ByteBuffer buffer) { + byte[] arr = value.getBytes(UTF8_CHARSET); + + buffer.put(arr); + buffer.put(NUL); // metadata strings are NUL-terminated + } + + @Override + public int calculateMarshalSize(String value) { + byte[] arr = value.getBytes(UTF8_CHARSET); + + return arr.length + 1; // metadata strings are NUL-terminated + } + + @Override + public String unmarshal(ByteBuffer buffer) { + buffer.mark(); // save the current position + + boolean foundNull = false; + int stringLength = 0; + while (buffer.hasRemaining()) { + if (buffer.get() == NUL) { + foundNull = true; + break; + } + + stringLength++; + } + + if (VERBOSE) { + Log.v(TAG, + "unmarshal - scanned " + stringLength + " characters; found null? " + + foundNull); + } + + if (!foundNull) { + throw new UnsupportedOperationException("Strings must be null-terminated"); + } + + buffer.reset(); // go back to the previously marked position + + byte[] strBytes = new byte[stringLength + 1]; + buffer.get(strBytes, /*dstOffset*/0, stringLength + 1); // including null character + + // not including null character + return new String(strBytes, /*offset*/0, stringLength, UTF8_CHARSET); + } + + @Override + public int getNativeSize() { + return NATIVE_SIZE_DYNAMIC; + } + } + + @Override + public Marshaler<String> createMarshaler( + TypeReference<String> managedType, int nativeType) { + return new MarshalerString(managedType, nativeType); + } + + @Override + public boolean isTypeMappingSupported(TypeReference<String> managedType, int nativeType) { + return nativeType == TYPE_BYTE && String.class.equals(managedType.getType()); + } +} diff --git a/core/java/android/hardware/camera2/marshal/impl/package.html b/core/java/android/hardware/camera2/marshal/impl/package.html new file mode 100644 index 0000000..783d0a1 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/impl/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body> diff --git a/core/java/android/hardware/camera2/marshal/package.html b/core/java/android/hardware/camera2/marshal/package.html new file mode 100644 index 0000000..783d0a1 --- /dev/null +++ b/core/java/android/hardware/camera2/marshal/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body> diff --git a/core/java/android/hardware/camera2/ColorSpaceTransform.java b/core/java/android/hardware/camera2/params/ColorSpaceTransform.java index 9912e4b..b4289db 100644 --- a/core/java/android/hardware/camera2/ColorSpaceTransform.java +++ b/core/java/android/hardware/camera2/params/ColorSpaceTransform.java @@ -14,10 +14,13 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; -import android.hardware.camera2.impl.HashCodeHelpers; + +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.utils.HashCodeHelpers; +import android.util.Rational; import java.util.Arrays; @@ -136,8 +139,8 @@ public final class ColorSpaceTransform { throw new IllegalArgumentException("row out of range"); } - int numerator = mElements[row * ROWS * RATIONAL_SIZE + column + OFFSET_NUMERATOR]; - int denominator = mElements[row * ROWS * RATIONAL_SIZE + column + OFFSET_DENOMINATOR]; + int numerator = mElements[(row * COLUMNS + column) * RATIONAL_SIZE + OFFSET_NUMERATOR]; + int denominator = mElements[(row * COLUMNS + column) * RATIONAL_SIZE + OFFSET_DENOMINATOR]; return new Rational(numerator, denominator); } @@ -159,7 +162,7 @@ public final class ColorSpaceTransform { public void copyElements(Rational[] destination, int offset) { checkArgumentNonnegative(offset, "offset must not be negative"); checkNotNull(destination, "destination must not be null"); - if (destination.length + offset < COUNT) { + if (destination.length - offset < COUNT) { throw new ArrayIndexOutOfBoundsException("destination too small to fit elements"); } @@ -194,7 +197,7 @@ public final class ColorSpaceTransform { public void copyElements(int[] destination, int offset) { checkArgumentNonnegative(offset, "offset must not be negative"); checkNotNull(destination, "destination must not be null"); - if (destination.length + offset < COUNT_INT) { + if (destination.length - offset < COUNT_INT) { throw new ArrayIndexOutOfBoundsException("destination too small to fit elements"); } diff --git a/core/java/android/hardware/camera2/Face.java b/core/java/android/hardware/camera2/params/Face.java index ded8839d..2cd83a3 100644 --- a/core/java/android/hardware/camera2/Face.java +++ b/core/java/android/hardware/camera2/params/Face.java @@ -15,10 +15,13 @@ */ -package android.hardware.camera2; +package android.hardware.camera2.params; import android.graphics.Point; import android.graphics.Rect; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureResult; /** * Describes a face detected in an image. diff --git a/core/java/android/hardware/camera2/LensShadingMap.java b/core/java/android/hardware/camera2/params/LensShadingMap.java index 2c224f6..9bbc33a 100644 --- a/core/java/android/hardware/camera2/LensShadingMap.java +++ b/core/java/android/hardware/camera2/params/LensShadingMap.java @@ -14,19 +14,21 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; -import static android.hardware.camera2.RggbChannelVector.*; +import static android.hardware.camera2.params.RggbChannelVector.*; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.utils.HashCodeHelpers; import java.util.Arrays; /** * Immutable class for describing a {@code 4 x N x M} lens shading map of floats. * - * @see CameraCharacteristics#LENS_SHADING_MAP + * @see CaptureResult#STATISTICS_LENS_SHADING_CORRECTION_MAP */ public final class LensShadingMap { @@ -60,12 +62,12 @@ public final class LensShadingMap { public LensShadingMap(final float[] elements, final int rows, final int columns) { mRows = checkArgumentPositive(rows, "rows must be positive"); - mColumns = checkArgumentPositive(rows, "columns must be positive"); + mColumns = checkArgumentPositive(columns, "columns must be positive"); mElements = checkNotNull(elements, "elements must not be null"); if (elements.length != getGainFactorCount()) { throw new IllegalArgumentException("elements must be " + getGainFactorCount() + - " length"); + " length, received " + elements.length); } // Every element must be finite and >= 1.0f @@ -240,4 +242,4 @@ public final class LensShadingMap { private final int mRows; private final int mColumns; private final float[] mElements; -}; +} diff --git a/core/java/android/hardware/camera2/MeteringRectangle.java b/core/java/android/hardware/camera2/params/MeteringRectangle.java index ff7a745..93fd053 100644 --- a/core/java/android/hardware/camera2/MeteringRectangle.java +++ b/core/java/android/hardware/camera2/params/MeteringRectangle.java @@ -13,32 +13,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.hardware.camera2; + +package android.hardware.camera2.params; import android.util.Size; import static com.android.internal.util.Preconditions.*; import android.graphics.Point; import android.graphics.Rect; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.utils.HashCodeHelpers; /** - * An immutable class to represent a rectangle {@code (x,y, width, height)} with an - * additional weight component. - * - * </p>The rectangle is defined to be inclusive of the specified coordinates.</p> - * - * <p>When used with a {@link CaptureRequest}, the coordinate system is based on the active pixel + * An immutable class to represent a rectangle {@code (x, y, width, height)} with an additional + * weight component. + * <p> + * The rectangle is defined to be inclusive of the specified coordinates. + * </p> + * <p> + * When used with a {@link CaptureRequest}, the coordinate system is based on the active pixel * array, with {@code (0,0)} being the top-left pixel in the * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE active pixel array}, and * {@code (android.sensor.info.activeArraySize.width - 1, - * android.sensor.info.activeArraySize.height - 1)} - * being the bottom-right pixel in the active pixel array. + * android.sensor.info.activeArraySize.height - 1)} being the bottom-right pixel in the active pixel + * array. + * </p> + * <p> + * The weight must range from {@value #METERING_WEIGHT_MIN} to {@value #METERING_WEIGHT_MAX} + * inclusively, and represents a weight for every pixel in the area. This means that a large + * metering area with the same weight as a smaller area will have more effect in the metering + * result. Metering areas can partially overlap and the camera device will add the weights in the + * overlap rectangle. + * </p> + * <p> + * If all rectangles have 0 weight, then no specific metering area needs to be used by the camera + * device. If the metering rectangle is outside the used android.scaler.cropRegion returned in + * capture result metadata, the camera device will ignore the sections outside the rectangle and + * output the used sections in the result metadata. * </p> - * - * <p>The metering weight is nonnegative.</p> */ public final class MeteringRectangle { + /** + * The minimum value of valid metering weight. + */ + public static final int METERING_WEIGHT_MIN = 0; + + /** + * The maximum value of valid metering weight. + */ + public static final int METERING_WEIGHT_MAX = 1000; + + /** + * Weights set to this value will cause the camera device to ignore this rectangle. + * If all metering rectangles are weighed with 0, the camera device will choose its own metering + * rectangles. + */ + public static final int METERING_WEIGHT_DONT_CARE = 0; private final int mX; private final int mY; @@ -53,16 +84,17 @@ public final class MeteringRectangle { * @param y coordinate >= 0 * @param width width >= 0 * @param height height >= 0 - * @param meteringWeight weight >= 0 - * - * @throws IllegalArgumentException if any of the parameters were non-negative + * @param meteringWeight weight between {@value #METERING_WEIGHT_MIN} and + * {@value #METERING_WEIGHT_MAX} inclusively + * @throws IllegalArgumentException if any of the parameters were negative */ public MeteringRectangle(int x, int y, int width, int height, int meteringWeight) { mX = checkArgumentNonnegative(x, "x must be nonnegative"); mY = checkArgumentNonnegative(y, "y must be nonnegative"); mWidth = checkArgumentNonnegative(width, "width must be nonnegative"); mHeight = checkArgumentNonnegative(height, "height must be nonnegative"); - mWeight = checkArgumentNonnegative(meteringWeight, "meteringWeight must be nonnegative"); + mWeight = checkArgumentInRange( + meteringWeight, METERING_WEIGHT_MIN, METERING_WEIGHT_MAX, "meteringWeight"); } /** @@ -72,7 +104,7 @@ public final class MeteringRectangle { * @param dimensions a non-{@code null} {@link android.util.Size Size} with width, height >= 0 * @param meteringWeight weight >= 0 * - * @throws IllegalArgumentException if any of the parameters were non-negative + * @throws IllegalArgumentException if any of the parameters were negative * @throws NullPointerException if any of the arguments were null */ public MeteringRectangle(Point xy, Size dimensions, int meteringWeight) { @@ -92,7 +124,7 @@ public final class MeteringRectangle { * @param rect a non-{@code null} rectangle with all x,y,w,h dimensions >= 0 * @param meteringWeight weight >= 0 * - * @throws IllegalArgumentException if any of the parameters were non-negative + * @throws IllegalArgumentException if any of the parameters were negative * @throws NullPointerException if any of the arguments were null */ public MeteringRectangle(Rect rect, int meteringWeight) { @@ -186,10 +218,7 @@ public final class MeteringRectangle { */ @Override public boolean equals(final Object other) { - if (other instanceof MeteringRectangle) { - return equals(other); - } - return false; + return other instanceof MeteringRectangle && equals((MeteringRectangle)other); } /** @@ -211,7 +240,7 @@ public final class MeteringRectangle { && mY == other.mY && mWidth == other.mWidth && mHeight == other.mHeight - && mWidth == other.mWidth); + && mWeight == other.mWeight); } /** diff --git a/core/java/android/hardware/camera2/ReprocessFormatsMap.java b/core/java/android/hardware/camera2/params/ReprocessFormatsMap.java index c6c59d4..d3f5bc3 100644 --- a/core/java/android/hardware/camera2/ReprocessFormatsMap.java +++ b/core/java/android/hardware/camera2/params/ReprocessFormatsMap.java @@ -14,11 +14,12 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.utils.HashCodeHelpers; import java.util.Arrays; @@ -61,9 +62,12 @@ public final class ReprocessFormatsMap { * @throws IllegalArgumentException * if the data was poorly formatted * (missing output format length or too few output formats) + * or if any of the input/formats were not valid * @throws NullPointerException * if entry was null * + * @see StreamConfigurationMap#checkArgumentFormatInternal + * * @hide */ public ReprocessFormatsMap(final int[] entry) { @@ -72,26 +76,31 @@ public final class ReprocessFormatsMap { int numInputs = 0; int left = entry.length; for (int i = 0; i < entry.length; ) { - final int format = entry[i]; + int inputFormat = StreamConfigurationMap.checkArgumentFormatInternal(entry[i]); left--; i++; if (left < 1) { throw new IllegalArgumentException( - String.format("Input %x had no output format length listed", format)); + String.format("Input %x had no output format length listed", inputFormat)); } final int length = entry[i]; left--; i++; + for (int j = 0; j < length; ++j) { + int outputFormat = entry[i + j]; + StreamConfigurationMap.checkArgumentFormatInternal(outputFormat); + } + if (length > 0) { if (left < length) { throw new IllegalArgumentException( String.format( "Input %x had too few output formats listed (actual: %d, " + - "expected: %d)", format, left, length)); + "expected: %d)", inputFormat, left, length)); } i += length; @@ -131,7 +140,6 @@ public final class ReprocessFormatsMap { throw new AssertionError( String.format("Input %x had no output format length listed", format)); } - // TODO: check format is a valid input format final int length = mEntry[i]; left--; @@ -149,12 +157,10 @@ public final class ReprocessFormatsMap { left -= length; } - // TODO: check output format is a valid output format - inputs[j] = format; } - return inputs; + return StreamConfigurationMap.imageFormatToPublic(inputs); } /** @@ -204,7 +210,7 @@ public final class ReprocessFormatsMap { outputs[k] = mEntry[i + k]; } - return outputs; + return StreamConfigurationMap.imageFormatToPublic(outputs); } i += length; diff --git a/core/java/android/hardware/camera2/RggbChannelVector.java b/core/java/android/hardware/camera2/params/RggbChannelVector.java index 80167c6..30591f6 100644 --- a/core/java/android/hardware/camera2/RggbChannelVector.java +++ b/core/java/android/hardware/camera2/params/RggbChannelVector.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; diff --git a/core/java/android/hardware/camera2/StreamConfiguration.java b/core/java/android/hardware/camera2/params/StreamConfiguration.java index c53dd7c..1c6b6e9 100644 --- a/core/java/android/hardware/camera2/StreamConfiguration.java +++ b/core/java/android/hardware/camera2/params/StreamConfiguration.java @@ -14,13 +14,16 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; -import static android.hardware.camera2.StreamConfigurationMap.checkArgumentFormatInternal; +import static android.hardware.camera2.params.StreamConfigurationMap.checkArgumentFormatInternal; import android.graphics.ImageFormat; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.utils.HashCodeHelpers; +import android.graphics.PixelFormat; import android.util.Size; /** @@ -57,16 +60,17 @@ public final class StreamConfiguration { final int format, final int width, final int height, final boolean input) { mFormat = checkArgumentFormatInternal(format); mWidth = checkArgumentPositive(width, "width must be positive"); - mHeight = checkArgumentPositive(width, "height must be positive"); + mHeight = checkArgumentPositive(height, "height must be positive"); mInput = input; } /** - * Get the image {@code format} in this stream configuration. + * Get the internal image {@code format} in this stream configuration. * * @return an integer format * * @see ImageFormat + * @see PixelFormat */ public final int getFormat() { return mFormat; diff --git a/core/java/android/hardware/camera2/StreamConfigurationDuration.java b/core/java/android/hardware/camera2/params/StreamConfigurationDuration.java index 189ae62..217059d 100644 --- a/core/java/android/hardware/camera2/StreamConfigurationDuration.java +++ b/core/java/android/hardware/camera2/params/StreamConfigurationDuration.java @@ -14,13 +14,15 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; -import static android.hardware.camera2.StreamConfigurationMap.checkArgumentFormatInternal; +import static android.hardware.camera2.params.StreamConfigurationMap.checkArgumentFormatInternal; import android.graphics.ImageFormat; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.utils.HashCodeHelpers; +import android.graphics.PixelFormat; import android.util.Size; /** @@ -54,16 +56,17 @@ public final class StreamConfigurationDuration { final int format, final int width, final int height, final long durationNs) { mFormat = checkArgumentFormatInternal(format); mWidth = checkArgumentPositive(width, "width must be positive"); - mHeight = checkArgumentPositive(width, "height must be positive"); + mHeight = checkArgumentPositive(height, "height must be positive"); mDurationNs = checkArgumentNonnegative(durationNs, "durationNs must be non-negative"); } /** - * Get the image {@code format} in this stream configuration duration + * Get the internal image {@code format} in this stream configuration duration * * @return an integer format * * @see ImageFormat + * @see PixelFormat */ public final int getFormat() { return mFormat; diff --git a/core/java/android/hardware/camera2/params/StreamConfigurationMap.java b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java new file mode 100644 index 0000000..4cd6d15 --- /dev/null +++ b/core/java/android/hardware/camera2/params/StreamConfigurationMap.java @@ -0,0 +1,949 @@ +/* + * 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.params; + +import android.graphics.ImageFormat; +import android.graphics.PixelFormat; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.utils.HashCodeHelpers; +import android.view.Surface; +import android.util.Log; +import android.util.Size; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Objects; + +import static com.android.internal.util.Preconditions.*; + +/** + * Immutable class to store the available stream + * {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP configurations} to be used + * when configuring streams with {@link CameraDevice#configureOutputs}. + * <!-- TODO: link to input stream configuration --> + * + * <p>This is the authoritative list for all <!-- input/ -->output formats (and sizes respectively + * for that format) that are supported by a camera device.</p> + * + * <p>This also contains the minimum frame durations and stall durations for each format/size + * combination that can be used to calculate effective frame rate when submitting multiple captures. + * </p> + * + * <p>An instance of this object is available from {@link CameraCharacteristics} using + * the {@link CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP} key and the + * {@link CameraCharacteristics#get} method.</p> + * + * <pre><code>{@code + * CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + * StreamConfigurationMap configs = characteristics.get( + * CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + * }</code></pre> + * + * @see CameraCharacteristics#SCALER_STREAM_CONFIGURATION_MAP + * @see CameraDevice#configureOutputs + */ +public final class StreamConfigurationMap { + + private static final String TAG = "StreamConfigurationMap"; + /** + * Create a new {@link StreamConfigurationMap}. + * + * <p>The array parameters ownership is passed to this object after creation; do not + * write to them after this constructor is invoked.</p> + * + * @param configurations a non-{@code null} array of {@link StreamConfiguration} + * @param minFrameDurations a non-{@code null} array of {@link StreamConfigurationDuration} + * @param stallDurations a non-{@code null} array of {@link StreamConfigurationDuration} + * + * @throws NullPointerException if any of the arguments or subelements were {@code null} + * + * @hide + */ + public StreamConfigurationMap( + StreamConfiguration[] configurations, + StreamConfigurationDuration[] minFrameDurations, + StreamConfigurationDuration[] stallDurations) { + + mConfigurations = checkArrayElementsNotNull(configurations, "configurations"); + mMinFrameDurations = checkArrayElementsNotNull(minFrameDurations, "minFrameDurations"); + mStallDurations = checkArrayElementsNotNull(stallDurations, "stallDurations"); + + // For each format, track how many sizes there are available to configure + for (StreamConfiguration config : configurations) { + HashMap<Integer, Integer> map = config.isOutput() ? mOutputFormats : mInputFormats; + + Integer count = map.get(config.getFormat()); + + if (count == null) { + count = 0; + } + count = count + 1; + + map.put(config.getFormat(), count); + } + + if (!mOutputFormats.containsKey(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED)) { + throw new AssertionError( + "At least one stream configuration for IMPLEMENTATION_DEFINED must exist"); + } + } + + /** + * Get the image {@code format} output formats in this stream configuration. + * + * <p>All image formats returned by this function will be defined in either {@link ImageFormat} + * or in {@link PixelFormat} (and there is no possibility of collision).</p> + * + * <p>Formats listed in this array are guaranteed to return true if queried with + * {@link #isOutputSupportedFor(int).</p> + * + * @return an array of integer format + * + * @see ImageFormat + * @see PixelFormat + */ + public final int[] getOutputFormats() { + return getPublicFormats(/*output*/true); + } + + /** + * Get the image {@code format} input formats in this stream configuration. + * + * <p>All image formats returned by this function will be defined in either {@link ImageFormat} + * or in {@link PixelFormat} (and there is no possibility of collision).</p> + * + * @return an array of integer format + * + * @see ImageFormat + * @see PixelFormat + * + * @hide + */ + public final int[] getInputFormats() { + return getPublicFormats(/*output*/false); + } + + /** + * Get the supported input sizes for this input format. + * + * <p>The format must have come from {@link #getInputFormats}; otherwise + * {@code null} is returned.</p> + * + * @param format a format from {@link #getInputFormats} + * @return a non-empty array of sizes, or {@code null} if the format was not available. + * + * @hide + */ + public Size[] getInputSizes(final int format) { + return getPublicFormatSizes(format, /*output*/false); + } + + /** + * Determine whether or not output streams can be + * {@link CameraDevice#configureOutputs configured} with a particular user-defined format. + * + * <p>This method determines that the output {@code format} is supported by the camera device; + * each output {@code surface} target may or may not itself support that {@code format}. + * Refer to the class which provides the surface for additional documentation.</p> + * + * <p>Formats for which this returns {@code true} are guaranteed to exist in the result + * returned by {@link #getOutputSizes}.</p> + * + * @param format an image format from either {@link ImageFormat} or {@link PixelFormat} + * @return + * {@code true} iff using a {@code surface} with this {@code format} will be + * supported with {@link CameraDevice#configureOutputs} + * + * @throws IllegalArgumentException + * if the image format was not a defined named constant + * from either {@link ImageFormat} or {@link PixelFormat} + * + * @see ImageFormat + * @see PixelFormat + * @see CameraDevice#configureOutputs + */ + public boolean isOutputSupportedFor(int format) { + checkArgumentFormat(format); + + format = imageFormatToInternal(format); + return getFormatsMap(/*output*/true).containsKey(format); + } + + /** + * Determine whether or not output streams can be configured with a particular class + * as a consumer. + * + * <p>The following list is generally usable for outputs: + * <ul> + * <li>{@link android.media.ImageReader} - + * Recommended for image processing or streaming to external resources (such as a file or + * network) + * <li>{@link android.media.MediaRecorder} - + * Recommended for recording video (simple to use) + * <li>{@link android.media.MediaCodec} - + * Recommended for recording video (more complicated to use, with more flexibility) + * <li>{@link android.renderscript.Allocation} - + * Recommended for image processing with {@link android.renderscript RenderScript} + * <li>{@link android.view.SurfaceHolder} - + * Recommended for low-power camera preview with {@link android.view.SurfaceView} + * <li>{@link android.graphics.SurfaceTexture} - + * Recommended for OpenGL-accelerated preview processing or compositing with + * {@link android.view.TextureView} + * </ul> + * </p> + * + * <p>Generally speaking this means that creating a {@link Surface} from that class <i>may</i> + * provide a producer endpoint that is suitable to be used with + * {@link CameraDevice#configureOutputs}.</p> + * + * <p>Since not all of the above classes support output of all format and size combinations, + * the particular combination should be queried with {@link #isOutputSupportedFor(Surface)}.</p> + * + * @param klass a non-{@code null} {@link Class} object reference + * @return {@code true} if this class is supported as an output, {@code false} otherwise + * + * @throws NullPointerException if {@code klass} was {@code null} + * + * @see CameraDevice#configureOutputs + * @see #isOutputSupportedFor(Surface) + */ + public static <T> boolean isOutputSupportedFor(Class<T> klass) { + checkNotNull(klass, "klass must not be null"); + + if (klass == android.media.ImageReader.class) { + return true; + } else if (klass == android.media.MediaRecorder.class) { + return true; + } else if (klass == android.media.MediaCodec.class) { + return true; + } else if (klass == android.renderscript.Allocation.class) { + return true; + } else if (klass == android.view.SurfaceHolder.class) { + return true; + } else if (klass == android.graphics.SurfaceTexture.class) { + return true; + } + + return false; + } + + /** + * Determine whether or not the {@code surface} in its current state is suitable to be + * {@link CameraDevice#configureOutputs configured} as an output. + * + * <p>Not all surfaces are usable with the {@link CameraDevice}, and not all configurations + * of that {@code surface} are compatible. Some classes that provide the {@code surface} are + * compatible with the {@link CameraDevice} in general + * (see {@link #isOutputSupportedFor(Class)}, but it is the caller's responsibility to put the + * {@code surface} into a state that will be compatible with the {@link CameraDevice}.</p> + * + * <p>Reasons for a {@code surface} being specifically incompatible might be: + * <ul> + * <li>Using a format that's not listed by {@link #getOutputFormats} + * <li>Using a format/size combination that's not listed by {@link #getOutputSizes} + * <li>The {@code surface} itself is not in a state where it can service a new producer.</p> + * </li> + * </ul> + * + * This is not an exhaustive list; see the particular class's documentation for further + * possible reasons of incompatibility.</p> + * + * @param surface a non-{@code null} {@link Surface} object reference + * @return {@code true} if this is supported, {@code false} otherwise + * + * @throws NullPointerException if {@code surface} was {@code null} + * + * @see CameraDevice#configureOutputs + * @see #isOutputSupportedFor(Class) + */ + public boolean isOutputSupportedFor(Surface surface) { + checkNotNull(surface, "surface must not be null"); + + throw new UnsupportedOperationException("Not implemented yet"); + + // TODO: JNI function that checks the Surface's IGraphicBufferProducer state + } + + /** + * Get a list of sizes compatible with {@code klass} to use as an output. + * + * <p>Since some of the supported classes may support additional formats beyond + * an opaque/implementation-defined (under-the-hood) format; this function only returns + * sizes for the implementation-defined format.</p> + * + * <p>Some classes such as {@link android.media.ImageReader} may only support user-defined + * formats; in particular {@link #isOutputSupportedFor(Class)} will return {@code true} for + * that class and this method will return an empty array (but not {@code null}).</p> + * + * <p>If a well-defined format such as {@code NV21} is required, use + * {@link #getOutputSizes(int)} instead.</p> + * + * <p>The {@code klass} should be a supported output, that querying + * {@code #isOutputSupportedFor(Class)} should return {@code true}.</p> + * + * @param klass + * a non-{@code null} {@link Class} object reference + * @return + * an array of supported sizes for implementation-defined formats, + * or {@code null} iff the {@code klass} is not a supported output + * + * @throws NullPointerException if {@code klass} was {@code null} + * + * @see #isOutputSupportedFor(Class) + */ + public <T> Size[] getOutputSizes(Class<T> klass) { + if (isOutputSupportedFor(klass) == false) { + return null; + } + + return getInternalFormatSizes(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED, /*output*/true); + } + + /** + * Get a list of sizes compatible with the requested image {@code format}. + * + * <p>The {@code format} should be a supported format (one of the formats returned by + * {@link #getOutputFormats}).</p> + * + * @param format an image format from {@link ImageFormat} or {@link PixelFormat} + * @return + * an array of supported sizes, + * or {@code null} if the {@code format} is not a supported output + * + * @see ImageFormat + * @see PixelFormat + * @see #getOutputFormats + */ + public Size[] getOutputSizes(int format) { + return getPublicFormatSizes(format, /*output*/true); + } + + /** + * Get the minimum {@link CaptureRequest#SENSOR_FRAME_DURATION frame duration} + * for the format/size combination (in nanoseconds). + * + * <p>{@code format} should be one of the ones returned by {@link #getOutputFormats()}.</p> + * <p>{@code size} should be one of the ones returned by + * {@link #getOutputSizes(int)}.</p> + * + * <p>This should correspond to the frame duration when only that stream is active, with all + * processing (typically in {@code android.*.mode}) set to either {@code OFF} or {@code FAST}. + * </p> + * + * <p>When multiple streams are used in a request, the minimum frame duration will be + * {@code max(individual stream min durations)}.</p> + * + * <!-- + * TODO: uncomment after adding input stream support + * <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> + * --> + * + * @param format an image format from {@link ImageFormat} or {@link PixelFormat} + * @param size an output-compatible size + * @return a minimum frame duration {@code >=} 0 in nanoseconds + * + * @throws IllegalArgumentException if {@code format} or {@code size} was not supported + * @throws NullPointerException if {@code size} was {@code null} + * + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see #getOutputStallDuration(int, Size) + * @see ImageFormat + * @see PixelFormat + */ + public long getOutputMinFrameDuration(int format, Size size) { + checkNotNull(size, "size must not be null"); + checkArgumentFormatSupported(format, /*output*/true); + + return getInternalFormatDuration(imageFormatToInternal(format), size, DURATION_MIN_FRAME); + } + + /** + * Get the minimum {@link CaptureRequest#SENSOR_FRAME_DURATION frame duration} + * for the class/size combination (in nanoseconds). + * + * <p>This assumes a the {@code klass} is set up to use an implementation-defined format. + * For user-defined formats, use {@link #getOutputMinFrameDuration(int, Size)}.</p> + * + * <p>{@code klass} should be one of the ones which is supported by + * {@link #isOutputSupportedFor(Class)}.</p> + * + * <p>{@code size} should be one of the ones returned by + * {@link #getOutputSizes(int)}.</p> + * + * <p>This should correspond to the frame duration when only that stream is active, with all + * processing (typically in {@code android.*.mode}) set to either {@code OFF} or {@code FAST}. + * </p> + * + * <p>When multiple streams are used in a request, the minimum frame duration will be + * {@code max(individual stream min durations)}.</p> + * + * <!-- + * TODO: uncomment after adding input stream support + * <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> + * --> + * + * @param klass + * a class which is supported by {@link #isOutputSupportedFor(Class)} and has a + * non-empty array returned by {@link #getOutputSizes(Class)} + * @param size an output-compatible size + * @return a minimum frame duration {@code >=} 0 in nanoseconds + * + * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported + * @throws NullPointerException if {@code size} or {@code klass} was {@code null} + * + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see ImageFormat + * @see PixelFormat + */ + public <T> long getOutputMinFrameDuration(final Class<T> klass, final Size size) { + if (!isOutputSupportedFor(klass)) { + throw new IllegalArgumentException("klass was not supported"); + } + + return getInternalFormatDuration(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED, + size, DURATION_MIN_FRAME); + } + + /** + * Get the stall duration for the format/size combination (in nanoseconds). + * + * <p>{@code format} should be one of the ones returned by {@link #getOutputFormats()}.</p> + * <p>{@code size} should be one of the ones returned by + * {@link #getOutputSizes(int)}.</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>For example, consider JPEG captures which have the following characteristics: + * + * <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>The JPEG processor can run concurrently to the rest of the camera pipeline, but cannot + * process more than 1 capture at a time. + * </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}) is the same as setting + * the minimum frame duration from the normal minimum frame duration corresponding to {@code S}, + * added with the maximum stall duration for {@code S}.</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 {@code android.*.mode}) set to {@code FAST} or {@code OFF}. + * Setting any of the processing modes to {@code 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: + * <ul> + * <li>{@link ImageFormat#JPEG JPEG} + * <li>{@link ImageFormat#RAW_SENSOR RAW16} + * </ul> + * </p> + * + * <p>The following formats will never have a stall duration: + * <ul> + * <li>{@link ImageFormat#YUV_420_888 YUV_420_888} + * <li>{@link #isOutputSupportedFor(Class) Implementation-Defined} + * </ul></p> + * + * <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> + * + * <p>See {@link CaptureRequest#SENSOR_FRAME_DURATION android.sensor.frameDuration} + * for more information about calculating the max frame rate (absent stalls).</p> + * + * @param format an image format from {@link ImageFormat} or {@link PixelFormat} + * @param size an output-compatible size + * @return a stall duration {@code >=} 0 in nanoseconds + * + * @throws IllegalArgumentException if {@code format} or {@code size} was not supported + * @throws NullPointerException if {@code size} was {@code null} + * + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see ImageFormat + * @see PixelFormat + */ + public long getOutputStallDuration(int format, Size size) { + checkArgumentFormatSupported(format, /*output*/true); + + return getInternalFormatDuration(imageFormatToInternal(format), + size, DURATION_STALL); + } + + /** + * Get the stall duration for the class/size combination (in nanoseconds). + * + * <p>This assumes a the {@code klass} is set up to use an implementation-defined format. + * For user-defined formats, use {@link #getOutputMinFrameDuration(int, Size)}.</p> + * + * <p>{@code klass} should be one of the ones with a non-empty array returned by + * {@link #getOutputSizes(Class)}.</p> + * + * <p>{@code size} should be one of the ones returned by + * {@link #getOutputSizes(Class)}.</p> + * + * <p>See {@link #getOutputStallDuration(int, Size)} for a definition of a + * <em>stall duration</em>.</p> + * + * @param klass + * a class which is supported by {@link #isOutputSupportedFor(Class)} and has a + * non-empty array returned by {@link #getOutputSizes(Class)} + * @param size an output-compatible size + * @return a minimum frame duration {@code >=} 0 in nanoseconds + * + * @throws IllegalArgumentException if {@code klass} or {@code size} was not supported + * @throws NullPointerException if {@code size} or {@code klass} was {@code null} + * + * @see CaptureRequest#SENSOR_FRAME_DURATION + * @see ImageFormat + * @see PixelFormat + */ + public <T> long getOutputStallDuration(final Class<T> klass, final Size size) { + if (!isOutputSupportedFor(klass)) { + throw new IllegalArgumentException("klass was not supported"); + } + + return getInternalFormatDuration(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED, + size, DURATION_STALL); + } + + /** + * Check if this {@link StreamConfigurationMap} is equal to another + * {@link StreamConfigurationMap}. + * + * <p>Two vectors are only equal if and only if each of the respective elements is equal.</p> + * + * @return {@code true} if the objects were equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object obj) { + if (obj == null) { + return false; + } + if (this == obj) { + return true; + } + if (obj instanceof StreamConfigurationMap) { + final StreamConfigurationMap other = (StreamConfigurationMap) obj; + // XX: do we care about order? + return Arrays.equals(mConfigurations, other.mConfigurations) && + Arrays.equals(mMinFrameDurations, other.mMinFrameDurations) && + Arrays.equals(mStallDurations, other.mStallDurations); + } + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + // XX: do we care about order? + return HashCodeHelpers.hashCode(mConfigurations, mMinFrameDurations, mStallDurations); + } + + // Check that the argument is supported by #getOutputFormats or #getInputFormats + private int checkArgumentFormatSupported(int format, boolean output) { + checkArgumentFormat(format); + + int[] formats = output ? getOutputFormats() : getInputFormats(); + for (int i = 0; i < formats.length; ++i) { + if (format == formats[i]) { + return format; + } + } + + throw new IllegalArgumentException(String.format( + "format %x is not supported by this stream configuration map", format)); + } + + /** + * Ensures that the format is either user-defined or implementation defined. + * + * <p>If a format has a different internal representation than the public representation, + * passing in the public representation here will fail.</p> + * + * <p>For example if trying to use {@link ImageFormat#JPEG}: + * it has a different public representation than the internal representation + * {@code HAL_PIXEL_FORMAT_BLOB}, this check will fail.</p> + * + * <p>Any invalid/undefined formats will raise an exception.</p> + * + * @param format image format + * @return the format + * + * @throws IllegalArgumentException if the format was invalid + */ + static int checkArgumentFormatInternal(int format) { + switch (format) { + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + case HAL_PIXEL_FORMAT_BLOB: + return format; + case ImageFormat.JPEG: + throw new IllegalArgumentException( + "ImageFormat.JPEG is an unknown internal format"); + default: + return checkArgumentFormat(format); + } + } + + /** + * Ensures that the format is publicly user-defined in either ImageFormat or PixelFormat. + * + * <p>If a format has a different public representation than the internal representation, + * passing in the internal representation here will fail.</p> + * + * <p>For example if trying to use {@code HAL_PIXEL_FORMAT_BLOB}: + * it has a different internal representation than the public representation + * {@link ImageFormat#JPEG}, this check will fail.</p> + * + * <p>Any invalid/undefined formats will raise an exception, including implementation-defined. + * </p> + * + * <p>Note that {@code @hide} and deprecated formats will not pass this check.</p> + * + * @param format image format + * @return the format + * + * @throws IllegalArgumentException if the format was not user-defined + */ + static int checkArgumentFormat(int format) { + // TODO: remove this hack , CTS shouldn't have been using internal constants + if (format == HAL_PIXEL_FORMAT_RAW_OPAQUE) { + Log.w(TAG, "RAW_OPAQUE is not yet a published format; allowing it anyway"); + return format; + } + + if (!ImageFormat.isPublicFormat(format) && !PixelFormat.isPublicFormat(format)) { + throw new IllegalArgumentException(String.format( + "format 0x%x was not defined in either ImageFormat or PixelFormat", format)); + } + + return format; + } + + /** + * Convert a public-visible {@code ImageFormat} into an internal format + * compatible with {@code graphics.h}. + * + * <p>In particular these formats are converted: + * <ul> + * <li>HAL_PIXEL_FORMAT_BLOB => ImageFormat.JPEG + * </ul> + * </p> + * + * <p>Passing in an implementation-defined format which has no public equivalent will fail; + * as will passing in a public format which has a different internal format equivalent. + * See {@link #checkArgumentFormat} for more details about a legal public format.</p> + * + * <p>All other formats are returned as-is, no further invalid check is performed.</p> + * + * <p>This function is the dual of {@link #imageFormatToInternal}.</p> + * + * @param format image format from {@link ImageFormat} or {@link PixelFormat} + * @return the converted image formats + * + * @throws IllegalArgumentException + * if {@code format} is {@code HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED} or + * {@link ImageFormat#JPEG} + * + * @see ImageFormat + * @see PixelFormat + * @see #checkArgumentFormat + */ + static int imageFormatToPublic(int format) { + switch (format) { + case HAL_PIXEL_FORMAT_BLOB: + return ImageFormat.JPEG; + case ImageFormat.JPEG: + throw new IllegalArgumentException( + "ImageFormat.JPEG is an unknown internal format"); + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + throw new IllegalArgumentException( + "IMPLEMENTATION_DEFINED must not leak to public API"); + default: + return format; + } + } + + /** + * Convert image formats from internal to public formats (in-place). + * + * @param formats an array of image formats + * @return {@code formats} + * + * @see #imageFormatToPublic + */ + static int[] imageFormatToPublic(int[] formats) { + if (formats == null) { + return null; + } + + for (int i = 0; i < formats.length; ++i) { + formats[i] = imageFormatToPublic(formats[i]); + } + + return formats; + } + + /** + * Convert a public format compatible with {@code ImageFormat} to an internal format + * from {@code graphics.h}. + * + * <p>In particular these formats are converted: + * <ul> + * <li>ImageFormat.JPEG => HAL_PIXEL_FORMAT_BLOB + * </ul> + * </p> + * + * <p>Passing in an implementation-defined format here will fail (it's not a public format); + * as will passing in an internal format which has a different public format equivalent. + * See {@link #checkArgumentFormat} for more details about a legal public format.</p> + * + * <p>All other formats are returned as-is, no invalid check is performed.</p> + * + * <p>This function is the dual of {@link #imageFormatToPublic}.</p> + * + * @param format public image format from {@link ImageFormat} or {@link PixelFormat} + * @return the converted image formats + * + * @see ImageFormat + * @see PixelFormat + * + * @throws IllegalArgumentException + * if {@code format} was {@code HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED} + */ + static int imageFormatToInternal(int format) { + switch (format) { + case ImageFormat.JPEG: + return HAL_PIXEL_FORMAT_BLOB; + case HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED: + throw new IllegalArgumentException( + "IMPLEMENTATION_DEFINED is not allowed via public API"); + default: + return format; + } + } + + /** + * Convert image formats from public to internal formats (in-place). + * + * @param formats an array of image formats + * @return {@code formats} + * + * @see #imageFormatToInternal + * + * @hide + */ + public static int[] imageFormatToInternal(int[] formats) { + if (formats == null) { + return null; + } + + for (int i = 0; i < formats.length; ++i) { + formats[i] = imageFormatToInternal(formats[i]); + } + + return formats; + } + + private Size[] getPublicFormatSizes(int format, boolean output) { + try { + checkArgumentFormatSupported(format, output); + } catch (IllegalArgumentException e) { + return null; + } + + format = imageFormatToInternal(format); + + return getInternalFormatSizes(format, output); + } + + private Size[] getInternalFormatSizes(int format, boolean output) { + HashMap<Integer, Integer> formatsMap = getFormatsMap(output); + + Integer sizesCount = formatsMap.get(format); + if (sizesCount == null) { + throw new IllegalArgumentException("format not available"); + } + + int len = sizesCount; + Size[] sizes = new Size[len]; + int sizeIndex = 0; + + for (StreamConfiguration config : mConfigurations) { + if (config.getFormat() == format && config.isOutput() == output) { + sizes[sizeIndex++] = config.getSize(); + } + } + + if (sizeIndex != len) { + throw new AssertionError( + "Too few sizes (expected " + len + ", actual " + sizeIndex + ")"); + } + + return sizes; + } + + /** Get the list of publically visible output formats; does not include IMPL_DEFINED */ + private int[] getPublicFormats(boolean output) { + int[] formats = new int[getPublicFormatCount(output)]; + + int i = 0; + + for (int format : getFormatsMap(output).keySet()) { + if (format != HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED) { + formats[i++] = format; + } + } + + if (formats.length != i) { + throw new AssertionError("Too few formats " + i + ", expected " + formats.length); + } + + return imageFormatToPublic(formats); + } + + /** Get the format -> size count map for either output or input formats */ + private HashMap<Integer, Integer> getFormatsMap(boolean output) { + return output ? mOutputFormats : mInputFormats; + } + + private long getInternalFormatDuration(int format, Size size, int duration) { + // assume format is already checked, since its internal + + if (!arrayContains(getInternalFormatSizes(format, /*output*/true), size)) { + throw new IllegalArgumentException("size was not supported"); + } + + StreamConfigurationDuration[] durations = getDurations(duration); + + for (StreamConfigurationDuration configurationDuration : durations) { + if (configurationDuration.getFormat() == format && + configurationDuration.getWidth() == size.getWidth() && + configurationDuration.getHeight() == size.getHeight()) { + return configurationDuration.getDuration(); + } + } + + return getDurationDefault(duration); + } + + /** + * Get the durations array for the kind of duration + * + * @see #DURATION_MIN_FRAME + * @see #DURATION_STALL + * */ + private StreamConfigurationDuration[] getDurations(int duration) { + switch (duration) { + case DURATION_MIN_FRAME: + return mMinFrameDurations; + case DURATION_STALL: + return mStallDurations; + default: + throw new IllegalArgumentException("duration was invalid"); + } + } + + private long getDurationDefault(int duration) { + switch (duration) { + case DURATION_MIN_FRAME: + throw new AssertionError("Minimum frame durations are required to be listed"); + case DURATION_STALL: + return 0L; // OK. A lack of a stall duration implies a 0 stall duration + default: + throw new IllegalArgumentException("duration was invalid"); + } + } + + /** Count the number of publicly-visible output formats */ + private int getPublicFormatCount(boolean output) { + HashMap<Integer, Integer> formatsMap = getFormatsMap(output); + + int size = formatsMap.size(); + if (formatsMap.containsKey(HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED)) { + size -= 1; + } + return size; + } + + private static <T> boolean arrayContains(T[] array, T element) { + if (array == null) { + return false; + } + + for (T el : array) { + if (Objects.equals(el, element)) { + return true; + } + } + + return false; + } + + // from system/core/include/system/graphics.h + private static final int HAL_PIXEL_FORMAT_BLOB = 0x21; + private static final int HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED = 0x22; + private static final int HAL_PIXEL_FORMAT_RAW_OPAQUE = 0x24; + + /** + * @see #getDurations(int) + * @see #getDurationDefault(int) + */ + private static final int DURATION_MIN_FRAME = 0; + private static final int DURATION_STALL = 1; + + private final StreamConfiguration[] mConfigurations; + private final StreamConfigurationDuration[] mMinFrameDurations; + private final StreamConfigurationDuration[] mStallDurations; + + /** ImageFormat -> num output sizes mapping */ + private final HashMap</*ImageFormat*/Integer, /*Count*/Integer> mOutputFormats = + new HashMap<Integer, Integer>(); + /** ImageFormat -> num input sizes mapping */ + private final HashMap</*ImageFormat*/Integer, /*Count*/Integer> mInputFormats = + new HashMap<Integer, Integer>(); + +} diff --git a/core/java/android/hardware/camera2/TonemapCurve.java b/core/java/android/hardware/camera2/params/TonemapCurve.java index ee20d68..0fcffac 100644 --- a/core/java/android/hardware/camera2/TonemapCurve.java +++ b/core/java/android/hardware/camera2/params/TonemapCurve.java @@ -14,12 +14,16 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; import android.graphics.PointF; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.utils.HashCodeHelpers; import java.util.Arrays; diff --git a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java index 328ccbe..40cda08 100644 --- a/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java +++ b/core/java/android/hardware/camera2/utils/CameraBinderDecorator.java @@ -40,6 +40,7 @@ public class CameraBinderDecorator { public static final int ALREADY_EXISTS = -17; public static final int BAD_VALUE = -22; public static final int DEAD_OBJECT = -32; + public static final int INVALID_OPERATION = -38; /** * TODO: add as error codes in Errors.h @@ -53,6 +54,7 @@ public class CameraBinderDecorator { public static final int EOPNOTSUPP = -95; public static final int EUSERS = -87; + private static class CameraBinderDecoratorListener implements Decorator.DecoratorListener { @Override @@ -125,6 +127,9 @@ public class CameraBinderDecorator { case EOPNOTSUPP: UncheckedThrow.throwAnyException(new CameraRuntimeException( CAMERA_DEPRECATED_HAL)); + case INVALID_OPERATION: + UncheckedThrow.throwAnyException(new IllegalStateException( + "Illegal state encountered in camera service.")); } /** diff --git a/core/java/android/hardware/camera2/impl/HashCodeHelpers.java b/core/java/android/hardware/camera2/utils/HashCodeHelpers.java index 2d63827..b980549 100644 --- a/core/java/android/hardware/camera2/impl/HashCodeHelpers.java +++ b/core/java/android/hardware/camera2/utils/HashCodeHelpers.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.hardware.camera2.impl; +package android.hardware.camera2.utils; /** * Provide hashing functions using the Modified Bernstein hash diff --git a/core/java/android/hardware/camera2/LongParcelable.aidl b/core/java/android/hardware/camera2/utils/LongParcelable.aidl index 7d7e51b..98ad1b2 100644 --- a/core/java/android/hardware/camera2/LongParcelable.aidl +++ b/core/java/android/hardware/camera2/utils/LongParcelable.aidl @@ -14,7 +14,7 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.utils; /** @hide */ -parcelable LongParcelable;
\ No newline at end of file +parcelable LongParcelable; diff --git a/core/java/android/hardware/camera2/LongParcelable.java b/core/java/android/hardware/camera2/utils/LongParcelable.java index 97b0631..c89b339 100644 --- a/core/java/android/hardware/camera2/LongParcelable.java +++ b/core/java/android/hardware/camera2/utils/LongParcelable.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.utils; import android.os.Parcel; import android.os.Parcelable; diff --git a/core/java/android/hardware/camera2/utils/TypeReference.java b/core/java/android/hardware/camera2/utils/TypeReference.java new file mode 100644 index 0000000..d0c919c --- /dev/null +++ b/core/java/android/hardware/camera2/utils/TypeReference.java @@ -0,0 +1,435 @@ +/* + * 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.utils; + +import java.lang.reflect.Array; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; + +import static com.android.internal.util.Preconditions.*; + +/** + * Super type token; allows capturing generic types at runtime by forcing them to be reified. + * + * <p>Usage example: <pre>{@code + * // using anonymous classes (preferred) + * TypeReference<Integer> intToken = new TypeReference<Integer>() {{ }}; + * + * // using named classes + * class IntTypeReference extends TypeReference<Integer> {...} + * TypeReference<Integer> intToken = new IntTypeReference(); + * }</p></pre> + * + * <p>Unlike the reference implementation, this bans nested TypeVariables; that is all + * dynamic types must equal to the static types.</p> + * + * <p>See <a href="http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html"> + * http://gafter.blogspot.com/2007/05/limitation-of-super-type-tokens.html</a> + * for more details.</p> + */ +public abstract class TypeReference<T> { + private final Type mType; + + /** + * Create a new type reference for {@code T}. + * + * @throws IllegalArgumentException if {@code T}'s actual type contains a type variable + * + * @see TypeReference + */ + protected TypeReference() { + ParameterizedType thisType = (ParameterizedType)getClass().getGenericSuperclass(); + + // extract the "T" from TypeReference<T> + mType = thisType.getActualTypeArguments()[0]; + + /* + * Prohibit type references with type variables such as + * + * class GenericListToken<T> extends TypeReference<List<T>> + * + * Since the "T" there is not known without an instance of T, type equality would + * consider *all* Lists equal regardless of T. Allowing this would defeat + * some of the type safety of a type reference. + */ + if (containsTypeVariable(mType)) { + throw new IllegalArgumentException( + "Including a type variable in a type reference is not allowed"); + } + } + + /** + * Return the dynamic {@link Type} corresponding to the captured type {@code T}. + */ + public Type getType() { + return mType; + } + + private TypeReference(Type type) { + mType = type; + + if (containsTypeVariable(mType)) { + throw new IllegalArgumentException( + "Including a type variable in a type reference is not allowed"); + } + } + + private static class SpecializedTypeReference<T> extends TypeReference<T> { + public SpecializedTypeReference(Class<T> klass) { + super(klass); + } + } + + @SuppressWarnings("rawtypes") + private static class SpecializedBaseTypeReference extends TypeReference { + public SpecializedBaseTypeReference(Type type) { + super(type); + } + } + + /** + * Create a specialized type reference from a dynamic class instance, + * bypassing the standard compile-time checks. + * + * <p>As with a regular type reference, the {@code klass} must not contain + * any type variables.</p> + * + * @param klass a non-{@code null} {@link Class} instance + * + * @return a type reference which captures {@code T} at runtime + * + * @throws IllegalArgumentException if {@code T} had any type variables + */ + public static <T> TypeReference<T> createSpecializedTypeReference(Class<T> klass) { + return new SpecializedTypeReference<T>(klass); + } + + /** + * Create a specialized type reference from a dynamic {@link Type} instance, + * bypassing the standard compile-time checks. + * + * <p>As with a regular type reference, the {@code type} must not contain + * any type variables.</p> + * + * @param type a non-{@code null} {@link Type} instance + * + * @return a type reference which captures {@code T} at runtime + * + * @throws IllegalArgumentException if {@code type} had any type variables + */ + public static TypeReference<?> createSpecializedTypeReference(Type type) { + return new SpecializedBaseTypeReference(type); + } + + /** + * Returns the raw type of T. + * + * <p><ul> + * <li>If T is a Class itself, T itself is returned. + * <li>If T is a ParameterizedType, the raw type of the parameterized type is returned. + * <li>If T is a GenericArrayType, the returned type is the corresponding array class. + * For example: {@code List<Integer>[]} => {@code List[]}. + * <li>If T is a type variable or a wildcard type, the raw type of the first upper bound is + * returned. For example: {@code <X extends Foo>} => {@code Foo}. + * </ul> + * + * @return the raw type of {@code T} + */ + @SuppressWarnings("unchecked") + public final Class<? super T> getRawType() { + return (Class<? super T>)getRawType(mType); + } + + private static final Class<?> getRawType(Type type) { + if (type == null) { + throw new NullPointerException("type must not be null"); + } + + if (type instanceof Class<?>) { + return (Class<?>)type; + } else if (type instanceof ParameterizedType) { + return (Class<?>)(((ParameterizedType)type).getRawType()); + } else if (type instanceof GenericArrayType) { + return getArrayClass(getRawType(((GenericArrayType)type).getGenericComponentType())); + } else if (type instanceof WildcardType) { + // Should be at most 1 upper bound, but treat it like an array for simplicity + return getRawType(((WildcardType) type).getUpperBounds()); + } else if (type instanceof TypeVariable) { + throw new AssertionError("Type variables are not allowed in type references"); + } else { + // Impossible + throw new AssertionError("Unhandled branch to get raw type for type " + type); + } + } + + private static final Class<?> getRawType(Type[] types) { + if (types == null) { + return null; + } + + for (Type type : types) { + Class<?> klass = getRawType(type); + if (klass != null) { + return klass; + } + } + + return null; + } + + private static final Class<?> getArrayClass(Class<?> componentType) { + return Array.newInstance(componentType, 0).getClass(); + } + + /** + * Get the component type, e.g. {@code T} from {@code T[]}. + * + * @return component type, or {@code null} if {@code T} is not an array + */ + public TypeReference<?> getComponentType() { + Type componentType = getComponentType(mType); + + return (componentType != null) ? + createSpecializedTypeReference(componentType) : + null; + } + + private static Type getComponentType(Type type) { + checkNotNull(type, "type must not be null"); + + if (type instanceof Class<?>) { + return ((Class<?>) type).getComponentType(); + } else if (type instanceof ParameterizedType) { + return null; + } else if (type instanceof GenericArrayType) { + return ((GenericArrayType)type).getGenericComponentType(); + } else if (type instanceof WildcardType) { + // Should be at most 1 upper bound, but treat it like an array for simplicity + throw new UnsupportedOperationException("TODO: support wild card components"); + } else if (type instanceof TypeVariable) { + throw new AssertionError("Type variables are not allowed in type references"); + } else { + // Impossible + throw new AssertionError("Unhandled branch to get component type for type " + type); + } + } + + /** + * Compare two objects for equality. + * + * <p>A TypeReference is only equal to another TypeReference if their captured type {@code T} + * is also equal.</p> + */ + @Override + public boolean equals(Object o) { + // Note that this comparison could inaccurately return true when comparing types + // with nested type variables; therefore we ban type variables in the constructor. + return o instanceof TypeReference<?> && mType.equals(((TypeReference<?>)o).mType); + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return mType.hashCode(); + } + + /** + * Check if the {@code type} contains a {@link TypeVariable} recursively. + * + * <p>Intuitively, a type variable is a type in a type expression that refers to a generic + * type which is not known at the definition of the expression (commonly seen when + * type parameters are used, e.g. {@code class Foo<T>}).</p> + * + * <p>See <a href="http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4"> + * http://docs.oracle.com/javase/specs/jls/se7/html/jls-4.html#jls-4.4</a> + * for a more formal definition of a type variable</p>. + * + * @param type a type object ({@code null} is allowed) + * @return {@code true} if there were nested type variables; {@code false} otherwise + */ + public static boolean containsTypeVariable(Type type) { + if (type == null) { + // Trivially false + return false; + } else if (type instanceof TypeVariable<?>) { + /* + * T -> trivially true + */ + return true; + } else if (type instanceof Class<?>) { + /* + * class Foo -> no type variable + * class Foo<T> - has a type variable + * + * This also covers the case of class Foo<T> extends ... / implements ... + * since everything on the right hand side would either include a type variable T + * or have no type variables. + */ + Class<?> klass = (Class<?>)type; + + // Empty array => class is not generic + if (klass.getTypeParameters().length != 0) { + return true; + } else { + // Does the outer class(es) contain any type variables? + + /* + * class Outer<T> { + * class Inner { + * T field; + * } + * } + * + * In this case 'Inner' has no type parameters itself, but it still has a type + * variable as part of the type definition. + */ + return containsTypeVariable(klass.getDeclaringClass()); + } + } else if (type instanceof ParameterizedType) { + /* + * This is the "Foo<T1, T2, T3, ... Tn>" in the scope of a + * + * // no type variables here, T1-Tn are known at this definition + * class X extends Foo<T1, T2, T3, ... Tn> + * + * // T1 is a type variable, T2-Tn are known at this definition + * class X<T1> extends Foo<T1, T2, T3, ... Tn> + */ + ParameterizedType p = (ParameterizedType) type; + + // This needs to be recursively checked + for (Type arg : p.getActualTypeArguments()) { + if (containsTypeVariable(arg)) { + return true; + } + } + + return false; + } else if (type instanceof WildcardType) { + WildcardType wild = (WildcardType) type; + + /* + * This is is the "?" inside of a + * + * Foo<?> --> unbounded; trivially no type variables + * Foo<? super T> --> lower bound; does T have a type variable? + * Foo<? extends T> --> upper bound; does T have a type variable? + */ + + /* + * According to JLS 4.5.1 + * (http://java.sun.com/docs/books/jls/third_edition/html/typesValues.html#4.5.1): + * + * - More than 1 lower/upper bound is illegal + * - Both a lower and upper bound is illegal + * + * However, we use this 'array OR array' approach for readability + */ + return containsTypeVariable(wild.getLowerBounds()) || + containsTypeVariable(wild.getUpperBounds()); + } + + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TypeReference<"); + toString(getType(), builder); + builder.append(">"); + + return builder.toString(); + } + + private static void toString(Type type, StringBuilder out) { + if (type == null) { + return; + } else if (type instanceof TypeVariable<?>) { + // T + out.append(((TypeVariable<?>)type).getName()); + } else if (type instanceof Class<?>) { + Class<?> klass = (Class<?>)type; + + out.append(klass.getName()); + toString(klass.getTypeParameters(), out); + } else if (type instanceof ParameterizedType) { + // "Foo<T1, T2, T3, ... Tn>" + ParameterizedType p = (ParameterizedType) type; + + out.append(((Class<?>)p.getRawType()).getName()); + toString(p.getActualTypeArguments(), out); + } else if (type instanceof GenericArrayType) { + GenericArrayType gat = (GenericArrayType)type; + + toString(gat.getGenericComponentType(), out); + out.append("[]"); + } else { // WildcardType, BoundedType + // TODO: + out.append(type.toString()); + } + } + + private static void toString(Type[] types, StringBuilder out) { + if (types == null) { + return; + } else if (types.length == 0) { + return; + } + + out.append("<"); + + for (int i = 0; i < types.length; ++i) { + toString(types[i], out); + if (i != types.length - 1) { + out.append(", "); + } + } + + out.append(">"); + } + + /** + * Check if any of the elements in this array contained a type variable. + * + * <p>Empty and null arrays trivially have no type variables.</p> + * + * @param typeArray an array ({@code null} is ok) of types + * @return true if any elements contained a type variable; false otherwise + */ + private static boolean containsTypeVariable(Type[] typeArray) { + if (typeArray == null) { + return false; + } + + for (Type type : typeArray) { + if (containsTypeVariable(type)) { + return true; + } + } + + return false; + } +} diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index cec90cd..e58c54d 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -156,6 +156,9 @@ public abstract class DisplayManagerInternal { // If true, enables automatic brightness control. public boolean useAutoBrightness; + //If true, scales the brightness to half of desired. + public boolean lowPowerMode; + // If true, prevents the screen from completely turning on if it is currently off. // The display does not enter a "ready" state if this flag is true and screen on is // blocked. The window manager policy blocks screen on while it prepares the keyguard to @@ -203,6 +206,7 @@ public abstract class DisplayManagerInternal { screenAutoBrightnessAdjustment = other.screenAutoBrightnessAdjustment; useAutoBrightness = other.useAutoBrightness; blockScreenOn = other.blockScreenOn; + lowPowerMode = other.lowPowerMode; } @Override @@ -218,7 +222,8 @@ public abstract class DisplayManagerInternal { && screenBrightness == other.screenBrightness && screenAutoBrightnessAdjustment == other.screenAutoBrightnessAdjustment && useAutoBrightness == other.useAutoBrightness - && blockScreenOn == other.blockScreenOn; + && blockScreenOn == other.blockScreenOn + && lowPowerMode == other.lowPowerMode; } @Override @@ -233,7 +238,8 @@ public abstract class DisplayManagerInternal { + ", screenBrightness=" + screenBrightness + ", screenAutoBrightnessAdjustment=" + screenAutoBrightnessAdjustment + ", useAutoBrightness=" + useAutoBrightness - + ", blockScreenOn=" + blockScreenOn; + + ", blockScreenOn=" + blockScreenOn + + ", lowPowerMode=" + lowPowerMode; } } diff --git a/core/java/android/hardware/hdmi/HdmiCec.java b/core/java/android/hardware/hdmi/HdmiCec.java index 7213c78..723eda1 100644 --- a/core/java/android/hardware/hdmi/HdmiCec.java +++ b/core/java/android/hardware/hdmi/HdmiCec.java @@ -120,7 +120,7 @@ public final class HdmiCec { public static final int MESSAGE_TIMER_CLEARED_STATUS = 0x043; public static final int MESSAGE_USER_CONTROL_PRESSED = 0x44; public static final int MESSAGE_USER_CONTROL_RELEASED = 0x45; - public static final int MESSAGE_GET_OSD_NAME = 0x46; + public static final int MESSAGE_GIVE_OSD_NAME = 0x46; public static final int MESSAGE_SET_OSD_NAME = 0x47; public static final int MESSAGE_SET_OSD_STRING = 0x64; public static final int MESSAGE_SET_TIMER_PROGRAM_TITLE = 0x67; @@ -158,6 +158,12 @@ public final class HdmiCec { public static final int MESSAGE_VENDOR_COMMAND_WITH_ID = 0xA0; public static final int MESSAGE_CLEAR_EXTERNAL_TIMER = 0xA1; public static final int MESSAGE_SET_EXTERNAL_TIMER = 0xA2; + public static final int MESSAGE_INITIATE_ARC = 0xC0; + public static final int MESSAGE_REPORT_ARC_INITIATED = 0xC1; + public static final int MESSAGE_REPORT_ARC_TERMINATED = 0xC2; + public static final int MESSAGE_REQUEST_ARC_INITIATION = 0xC3; + public static final int MESSAGE_REQUEST_ARC_TERMINATION = 0xC4; + public static final int MESSAGE_TERMINATE_ARC = 0xC5; public static final int MESSAGE_ABORT = 0xFF; public static final int UNKNOWN_VENDOR_ID = 0xFFFFFF; @@ -165,8 +171,15 @@ public final class HdmiCec { public static final int POWER_STATUS_UNKNOWN = -1; public static final int POWER_STATUS_ON = 0; public static final int POWER_STATUS_STANDBY = 1; - public static final int POWER_TRANSIENT_TO_ON = 2; - public static final int POWER_TRANSIENT_TO_STANDBY = 3; + public static final int POWER_STATUS_TRANSIENT_TO_ON = 2; + public static final int POWER_STATUS_TRANSIENT_TO_STANDBY = 3; + + public static final int RESULT_SUCCESS = 0; + public static final int RESULT_TIMEOUT = 1; + public static final int RESULT_SOURCE_NOT_AVAILABLE = 2; + public static final int RESULT_TARGET_NOT_AVAILABLE = 3; + public static final int RESULT_ALREADY_IN_PROGRESS = 4; + public static final int RESULT_EXCEPTION = 5; private static final int[] ADDRESS_TO_TYPE = { DEVICE_TV, // ADDR_TV @@ -181,6 +194,8 @@ public final class HdmiCec { DEVICE_RECORDER, // ADDR_RECORDER_3 DEVICE_TUNER, // ADDR_TUNER_4 DEVICE_PLAYBACK, // ADDR_PLAYBACK_3 + DEVICE_RESERVED, + DEVICE_RESERVED, DEVICE_TV, // ADDR_SPECIFIC_USE }; @@ -197,6 +212,8 @@ public final class HdmiCec { "Recorder_3", "Tuner_4", "Playback_3", + "Reserved_1", + "Reserved_2", "Secondary_TV", }; diff --git a/core/java/android/hardware/hdmi/HdmiCecClient.java b/core/java/android/hardware/hdmi/HdmiCecClient.java index cd86cd8..dcb3624 100644 --- a/core/java/android/hardware/hdmi/HdmiCecClient.java +++ b/core/java/android/hardware/hdmi/HdmiCecClient.java @@ -69,44 +69,28 @@ public final class HdmiCecClient { * Send <Active Source> message. */ public void sendActiveSource() { - try { - mService.sendActiveSource(mBinder); - } catch (RemoteException e) { - Log.e(TAG, "sendActiveSource threw exception ", e); - } + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); } /** * Send <Inactive Source> message. */ public void sendInactiveSource() { - try { - mService.sendInactiveSource(mBinder); - } catch (RemoteException e) { - Log.e(TAG, "sendInactiveSource threw exception ", e); - } + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); } /** * Send <Text View On> message. */ public void sendTextViewOn() { - try { - mService.sendTextViewOn(mBinder); - } catch (RemoteException e) { - Log.e(TAG, "sendTextViewOn threw exception ", e); - } + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); } /** * Send <Image View On> message. */ public void sendImageViewOn() { - try { - mService.sendImageViewOn(mBinder); - } catch (RemoteException e) { - Log.e(TAG, "sendImageViewOn threw exception ", e); - } + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); } /** @@ -116,11 +100,7 @@ public final class HdmiCecClient { * {@link HdmiCec#ADDR_TV}. */ public void sendGiveDevicePowerStatus(int address) { - try { - mService.sendGiveDevicePowerStatus(mBinder, address); - } catch (RemoteException e) { - Log.e(TAG, "sendGiveDevicePowerStatus threw exception ", e); - } + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); } /** @@ -133,11 +113,7 @@ public final class HdmiCecClient { * @return true if TV is on; otherwise false. */ public boolean isTvOn() { - try { - return mService.isTvOn(mBinder); - } catch (RemoteException e) { - Log.e(TAG, "isTvOn threw exception ", e); - } - return false; + Log.w(TAG, "In transition to HdmiControlManager. Will not work."); + return true; } } diff --git a/core/java/android/hardware/hdmi/HdmiCecManager.java b/core/java/android/hardware/hdmi/HdmiCecManager.java index 10b058c..03c46d8 100644 --- a/core/java/android/hardware/hdmi/HdmiCecManager.java +++ b/core/java/android/hardware/hdmi/HdmiCecManager.java @@ -45,15 +45,7 @@ public final class HdmiCecManager { * @return {@link HdmiCecClient} instance. {@code null} on failure. */ public HdmiCecClient getClient(int type, HdmiCecClient.Listener listener) { - if (mService == null) { - return null; - } - try { - IBinder b = mService.allocateLogicalDevice(type, getListenerWrapper(listener)); - return HdmiCecClient.create(mService, b); - } catch (RemoteException e) { - return null; - } + return HdmiCecClient.create(mService, null); } private IHdmiCecListener getListenerWrapper(final HdmiCecClient.Listener listener) { diff --git a/core/java/android/hardware/hdmi/HdmiCecMessage.java b/core/java/android/hardware/hdmi/HdmiCecMessage.java index ddaf870..62fa279 100644 --- a/core/java/android/hardware/hdmi/HdmiCecMessage.java +++ b/core/java/android/hardware/hdmi/HdmiCecMessage.java @@ -46,7 +46,7 @@ public final class HdmiCecMessage implements Parcelable { public HdmiCecMessage(int source, int destination, int opcode, byte[] params) { mSource = source; mDestination = destination; - mOpcode = opcode; + mOpcode = opcode & 0xFF; mParams = Arrays.copyOf(params, params.length); } @@ -123,6 +123,7 @@ public final class HdmiCecMessage implements Parcelable { * @param p HdmiCecMessage object to read the Rating from * @return a new HdmiCecMessage created from the data in the parcel */ + @Override public HdmiCecMessage createFromParcel(Parcel p) { int source = p.readInt(); int destination = p.readInt(); @@ -131,6 +132,7 @@ public final class HdmiCecMessage implements Parcelable { p.readByteArray(params); return new HdmiCecMessage(source, destination, opcode, params); } + @Override public HdmiCecMessage[] newArray(int size) { return new HdmiCecMessage[size]; } @@ -139,11 +141,40 @@ public final class HdmiCecMessage implements Parcelable { @Override public String toString() { StringBuffer s = new StringBuffer(); - s.append(String.format("src: %d dst: %d op: %2X params: ", mSource, mDestination, mOpcode)); - for (byte data : mParams) { - s.append(String.format("%02X ", data)); + s.append(String.format("<%s> src: %d, dst: %d", + opcodeToString(mOpcode), mSource, mDestination)); + if (mParams.length > 0) { + s.append(", params:"); + for (byte data : mParams) { + s.append(String.format(" %02X", data)); + } } return s.toString(); } + + private static String opcodeToString(int opcode) { + switch (opcode) { + case HdmiCec.MESSAGE_FEATURE_ABORT: + return "Feature Abort"; + case HdmiCec.MESSAGE_CEC_VERSION: + return "CEC Version"; + case HdmiCec.MESSAGE_REQUEST_ARC_INITIATION: + return "Request ARC Initiation"; + case HdmiCec.MESSAGE_REQUEST_ARC_TERMINATION: + return "Request ARC Termination"; + case HdmiCec.MESSAGE_REPORT_ARC_INITIATED: + return "Report ARC Initiated"; + case HdmiCec.MESSAGE_REPORT_ARC_TERMINATED: + return "Report ARC Terminated"; + case HdmiCec.MESSAGE_TEXT_VIEW_ON: + return "Text View On"; + case HdmiCec.MESSAGE_ACTIVE_SOURCE: + return "Active Source"; + case HdmiCec.MESSAGE_GIVE_DEVICE_POWER_STATUS: + return "Give Device Power Status"; + default: + return String.format("Opcode: %02X", opcode); + } + } } diff --git a/core/java/android/hardware/hdmi/HdmiControlManager.java b/core/java/android/hardware/hdmi/HdmiControlManager.java new file mode 100644 index 0000000..5b6e862 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiControlManager.java @@ -0,0 +1,157 @@ +/* + * 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.annotation.Nullable; +import android.os.RemoteException; + +/** + * The {@link HdmiControlManager} class is used to send HDMI control messages + * to attached CEC devices. + * + * <p>Provides various HDMI client instances that represent HDMI-CEC logical devices + * hosted in the system. {@link #getTvClient()}, for instance will return an + * {@link HdmiTvClient} object if the system is configured to host one. Android system + * can host more than one logical CEC devices. If multiple types are configured they + * all work as if they were independent logical devices running in the system. + */ +public final class HdmiControlManager { + @Nullable private final IHdmiControlService mService; + + // True if we have a logical device of type playback hosted in the system. + private final boolean mHasPlaybackDevice; + // True if we have a logical device of type TV hosted in the system. + private final boolean mHasTvDevice; + + /** + * @hide - hide this constructor because it has a parameter of type + * IHdmiControlService, which is a system private class. The right way + * to create an instance of this class is using the factory + * Context.getSystemService. + */ + public HdmiControlManager(IHdmiControlService service) { + mService = service; + int[] types = null; + if (mService != null) { + try { + types = mService.getSupportedTypes(); + } catch (RemoteException e) { + // Do nothing. + } + } + mHasTvDevice = hasDeviceType(types, HdmiCec.DEVICE_TV); + mHasPlaybackDevice = hasDeviceType(types, HdmiCec.DEVICE_PLAYBACK); + } + + private static boolean hasDeviceType(int[] types, int type) { + if (types == null) { + return false; + } + for (int t : types) { + if (t == type) { + return true; + } + } + return false; + } + + /** + * Gets an object that represents a HDMI-CEC logical device of type playback on the system. + * + * <p>Used to send HDMI control messages to other devices like TV or audio amplifier through + * HDMI bus. It is also possible to communicate with other logical devices hosted in the same + * system if the system is configured to host more than one type of HDMI-CEC logical devices. + * + * @return {@link HdmiPlaybackClient} instance. {@code null} on failure. + */ + @Nullable + public HdmiPlaybackClient getPlaybackClient() { + if (mService == null || !mHasPlaybackDevice) { + return null; + } + return new HdmiPlaybackClient(mService); + } + + /** + * Gets an object that represents a HDMI-CEC logical device of type TV on the system. + * + * <p>Used to send HDMI control messages to other devices and manage them through + * HDMI bus. It is also possible to communicate with other logical devices hosted in the same + * system if the system is configured to host more than one type of HDMI-CEC logical devices. + * + * @return {@link HdmiTvClient} instance. {@code null} on failure. + */ + @Nullable + public HdmiTvClient getTvClient() { + if (mService == null || !mHasTvDevice) { + return null; + } + return new HdmiTvClient(mService); + } + + /** + * Listener used to get hotplug event from HDMI port. + */ + public interface HotplugEventListener { + void onReceived(HdmiHotplugEvent event); + } + + /** + * Adds a listener to get informed of {@link HdmiHotplugEvent}. + * + * <p>To stop getting the notification, + * use {@link #removeHotplugEventListener(HotplugEventListener)}. + * + * @param listener {@link HotplugEventListener} instance + * @see HdmiControlManager#removeHotplugEventListener(HotplugEventListener) + */ + public void addHotplugEventListener(HotplugEventListener listener) { + if (mService == null) { + return; + } + try { + mService.addHotplugEventListener(getHotplugEventListenerWrapper(listener)); + } catch (RemoteException e) { + // Do nothing. + } + } + + /** + * Removes a listener to stop getting informed of {@link HdmiHotplugEvent}. + * + * @param listener {@link HotplugEventListener} instance to be removed + */ + public void removeHotplugEventListener(HotplugEventListener listener) { + if (mService == null) { + return; + } + try { + mService.removeHotplugEventListener(getHotplugEventListenerWrapper(listener)); + } catch (RemoteException e) { + // Do nothing. + } + } + + private IHdmiHotplugEventListener getHotplugEventListenerWrapper( + final HotplugEventListener listener) { + return new IHdmiHotplugEventListener.Stub() { + public void onReceived(HdmiHotplugEvent event) { + listener.onReceived(event);; + } + }; + } +} diff --git a/core/java/android/hardware/hdmi/HdmiHotplugEvent.aidl b/core/java/android/hardware/hdmi/HdmiHotplugEvent.aidl new file mode 100644 index 0000000..3117dd6 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiHotplugEvent.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.hdmi; + +parcelable HdmiHotplugEvent; diff --git a/core/java/android/hardware/hdmi/HdmiHotplugEvent.java b/core/java/android/hardware/hdmi/HdmiHotplugEvent.java new file mode 100644 index 0000000..1462f83 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiHotplugEvent.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.hdmi; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class that describes the HDMI port hotplug event. + */ +public final class HdmiHotplugEvent implements Parcelable { + + private final int mPort; + private final boolean mConnected; + + /** + * Constructor. + * + * <p>Marked as hidden so only system can create the instance. + * + * @hide + */ + public HdmiHotplugEvent(int port, boolean connected) { + mPort = port; + mConnected = connected; + } + + /** + * Return the port number for which the event occurred. + * + * @return port number + */ + public int getPort() { + return mPort; + } + + /** + * Return the connection status associated with this event + * + * @return true if the device gets connected; otherwise false + */ + public boolean isConnected() { + return mConnected; + } + + /** + * Describe the kinds of special objects contained in this Parcelable's + * marshalled representation. + */ + @Override + public int describeContents() { + return 0; + } + + /** + * Flatten this object in to a 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(mPort); + dest.writeByte((byte) (mConnected ? 1 : 0)); + } + + public static final Parcelable.Creator<HdmiHotplugEvent> CREATOR + = new Parcelable.Creator<HdmiHotplugEvent>() { + /** + * Rebuild a {@link HdmiHotplugEvent} previously stored with + * {@link Parcelable#writeToParcel(Parcel, int)}. + * + * @param p {@link HdmiHotplugEvent} object to read the Rating from + * @return a new {@link HdmiHotplugEvent} created from the data in the parcel + */ + public HdmiHotplugEvent createFromParcel(Parcel p) { + int port = p.readInt(); + boolean connected = p.readByte() == 1; + return new HdmiHotplugEvent(port, connected); + } + public HdmiHotplugEvent[] newArray(int size) { + return new HdmiHotplugEvent[size]; + } + }; +} diff --git a/core/java/android/hardware/hdmi/HdmiPlaybackClient.java b/core/java/android/hardware/hdmi/HdmiPlaybackClient.java new file mode 100644 index 0000000..f0bd237 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiPlaybackClient.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 android.hardware.hdmi; + +import android.os.RemoteException; + +import android.util.Log; + +/** + * HdmiPlaybackClient represents HDMI-CEC logical device of type Playback + * in the Android system which acts as a playback device such as set-top box. + * It provides with methods that control, get information from TV/Display device + * connected through HDMI bus. + */ +public final class HdmiPlaybackClient { + private static final String TAG = "HdmiPlaybackClient"; + + private final IHdmiControlService mService; + + /** + * Listener used by the client to get the result of one touch play operation. + */ + public interface OneTouchPlayCallback { + /** + * Called when the result of the feature one touch play is returned. + * + * @param result the result of the operation. {@link HdmiCec#RESULT_SUCCESS} + * if successful. + */ + public void onComplete(int result); + } + + /** + * Listener used by the client to get display device status. + */ + public interface DisplayStatusCallback { + /** + * Called when display device status is reported. + * + * @param status display device status + * @see {@link HdmiCec#POWER_STATUS_ON} + * @see {@link HdmiCec#POWER_STATUS_STANDBY} + * @see {@link HdmiCec#POWER_STATUS_TRANSIENT_TO_ON} + * @see {@link HdmiCec#POWER_STATUS_TRANSIENT_TO_STANDBY} + * @see {@link HdmiCec#POWER_STATUS_UNKNOWN} + */ + public void onComplete(int status); + } + + HdmiPlaybackClient(IHdmiControlService service) { + mService = service; + } + + /** + * Perform the feature 'one touch play' from playback device to turn on display + * and switch the input. + * + * @param callback {@link OneTouchPlayCallback} object to get informed + * of the result + */ + public void oneTouchPlay(OneTouchPlayCallback callback) { + // TODO: Use PendingResult. + try { + mService.oneTouchPlay(getCallbackWrapper(callback)); + } catch (RemoteException e) { + Log.e(TAG, "oneTouchPlay threw exception ", e); + } + } + + /** + * Get the status of display device connected through HDMI bus. + * + * @param callback {@link DisplayStatusCallback} object to get informed + * of the result + */ + public void queryDisplayStatus(DisplayStatusCallback callback) { + // TODO: PendingResult. + try { + mService.queryDisplayStatus(getCallbackWrapper(callback)); + } catch (RemoteException e) { + Log.e(TAG, "queryDisplayStatus threw exception ", e); + } + } + + private IHdmiControlCallback getCallbackWrapper(final OneTouchPlayCallback callback) { + return new IHdmiControlCallback.Stub() { + @Override + public void onComplete(int result) { + callback.onComplete(result); + } + }; + } + + private IHdmiControlCallback getCallbackWrapper(final DisplayStatusCallback callback) { + return new IHdmiControlCallback.Stub() { + @Override + public void onComplete(int status) { + callback.onComplete(status); + } + }; + } +} diff --git a/core/java/android/hardware/hdmi/HdmiTvClient.java b/core/java/android/hardware/hdmi/HdmiTvClient.java new file mode 100644 index 0000000..73c7247 --- /dev/null +++ b/core/java/android/hardware/hdmi/HdmiTvClient.java @@ -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.hardware.hdmi; + +/** + * HdmiTvClient represents HDMI-CEC logical device of type TV in the Android system + * which acts as TV/Display. It provides with methods that manage, interact with other + * devices on the CEC bus. + */ +public final class HdmiTvClient { + private static final String TAG = "HdmiTvClient"; + + private final IHdmiControlService mService; + + HdmiTvClient(IHdmiControlService service) { + mService = service; + } +} diff --git a/core/java/android/tv/ITvInputSessionCallback.aidl b/core/java/android/hardware/hdmi/IHdmiControlCallback.aidl index a2bd0d7..ef3dd47 100644 --- a/core/java/android/tv/ITvInputSessionCallback.aidl +++ b/core/java/android/hardware/hdmi/IHdmiControlCallback.aidl @@ -14,15 +14,14 @@ * limitations under the License. */ -package android.tv; - -import android.tv.ITvInputSession; +package android.hardware.hdmi; /** - * Helper interface for ITvInputSession to allow the TV input to notify the system service when a - * new session has been created. + * Callback interface definition for HDMI client to get informed of + * the result of various API invocation. + * * @hide */ -oneway interface ITvInputSessionCallback { - void onSessionCreated(ITvInputSession session); +oneway interface IHdmiControlCallback { + void onComplete(int result); } diff --git a/core/java/android/hardware/hdmi/IHdmiControlService.aidl b/core/java/android/hardware/hdmi/IHdmiControlService.aidl new file mode 100644 index 0000000..8da38e1 --- /dev/null +++ b/core/java/android/hardware/hdmi/IHdmiControlService.aidl @@ -0,0 +1,36 @@ +/* + * 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.hardware.hdmi.HdmiCecMessage; +import android.hardware.hdmi.IHdmiControlCallback; +import android.hardware.hdmi.IHdmiHotplugEventListener; + +/** + * Binder interface that clients running in the application process + * will use to perform HDMI-CEC features by communicating with other devices + * on the bus. + * + * @hide + */ +interface IHdmiControlService { + int[] getSupportedTypes(); + void oneTouchPlay(IHdmiControlCallback callback); + void queryDisplayStatus(IHdmiControlCallback callback); + void addHotplugEventListener(IHdmiHotplugEventListener listener); + void removeHotplugEventListener(IHdmiHotplugEventListener listener); +} diff --git a/core/java/android/tv/ITvInputServiceCallback.aidl b/core/java/android/hardware/hdmi/IHdmiHotplugEventListener.aidl index e535c81..5d63264 100644 --- a/core/java/android/tv/ITvInputServiceCallback.aidl +++ b/core/java/android/hardware/hdmi/IHdmiHotplugEventListener.aidl @@ -14,15 +14,16 @@ * limitations under the License. */ -package android.tv; +package android.hardware.hdmi; -import android.content.ComponentName; +import android.hardware.hdmi.HdmiHotplugEvent; /** - * Helper interface for ITvInputService to allow the TV input to notify the client when its status - * has been changed. + * Callback interface definition for HDMI client to get informed of + * the result of various API invocation. + * * @hide */ -oneway interface ITvInputServiceCallback { - void onAvailabilityChanged(in ComponentName name, boolean isAvailable); +oneway interface IHdmiHotplugEventListener { + void onReceived(in HdmiHotplugEvent event); } diff --git a/core/java/android/hardware/usb/UsbConfiguration.java b/core/java/android/hardware/usb/UsbConfiguration.java index 92d6f75..da5c128 100644 --- a/core/java/android/hardware/usb/UsbConfiguration.java +++ b/core/java/android/hardware/usb/UsbConfiguration.java @@ -44,13 +44,13 @@ public class UsbConfiguration implements Parcelable { * Mask for "self-powered" bit in the configuration's attributes. * @see #getAttributes */ - public static final int ATTR_SELF_POWERED_MASK = 1 << 6; + private static final int ATTR_SELF_POWERED = 1 << 6; /** * Mask for "remote wakeup" bit in the configuration's attributes. * @see #getAttributes */ - public static final int ATTR_REMOTE_WAKEUP_MASK = 1 << 5; + private static final int ATTR_REMOTE_WAKEUP = 1 << 5; /** * UsbConfiguration should only be instantiated by UsbService implementation @@ -83,19 +83,23 @@ public class UsbConfiguration implements Parcelable { } /** - * Returns the configuration's attributes field. - * This field contains a bit field with the following flags: + * Returns the self-powered attribute value configuration's attributes field. + * This attribute indicates that the device has a power source other than the USB connection. * - * 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 + * @return the configuration's self-powered attribute */ - public int getAttributes() { - return mAttributes; + public boolean isSelfPowered() { + return (mAttributes & ATTR_SELF_POWERED) != 0; + } + + /** + * Returns the remote-wakeup attribute value configuration's attributes field. + * This attributes that the device may signal the host to wake from suspend. + * + * @return the configuration's remote-wakeup attribute + */ + public boolean isRemoteWakeup() { + return (mAttributes & ATTR_REMOTE_WAKEUP) != 0; } /** diff --git a/core/java/android/hardware/usb/UsbDeviceConnection.java b/core/java/android/hardware/usb/UsbDeviceConnection.java index 6283951..c062b3a 100644 --- a/core/java/android/hardware/usb/UsbDeviceConnection.java +++ b/core/java/android/hardware/usb/UsbDeviceConnection.java @@ -104,7 +104,7 @@ 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 + * @return true if the interface was successfully selected */ public boolean setInterface(UsbInterface intf) { return native_set_interface(intf.getId(), intf.getAlternateSetting()); diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java index 06d8e4a..857e335 100644 --- a/core/java/android/inputmethodservice/IInputMethodWrapper.java +++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java @@ -69,6 +69,7 @@ class IInputMethodWrapper extends IInputMethod.Stub private static final int DO_CHANGE_INPUTMETHOD_SUBTYPE = 80; final WeakReference<AbstractInputMethodService> mTarget; + final Context mContext; final HandlerCaller mCaller; final WeakReference<InputMethod> mInputMethod; final int mTargetSdkVersion; @@ -111,8 +112,8 @@ class IInputMethodWrapper extends IInputMethod.Stub public IInputMethodWrapper(AbstractInputMethodService context, InputMethod inputMethod) { mTarget = new WeakReference<AbstractInputMethodService>(context); - mCaller = new HandlerCaller(context.getApplicationContext(), null, - this, true /*asyncHandler*/); + mContext = context.getApplicationContext(); + mCaller = new HandlerCaller(mContext, null, this, true /*asyncHandler*/); mInputMethod = new WeakReference<InputMethod>(inputMethod); mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion; } @@ -186,7 +187,7 @@ class IInputMethodWrapper extends IInputMethod.Stub case DO_CREATE_SESSION: { SomeArgs args = (SomeArgs)msg.obj; inputMethod.createSession(new InputMethodSessionCallbackWrapper( - mCaller.mContext, (InputChannel)args.arg1, + mContext, (InputChannel)args.arg1, (IInputSessionCallback)args.arg2)); args.recycle(); return; diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 4bccaf1..3417de1 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -39,6 +39,7 @@ import android.text.method.MovementMethod; import android.util.Log; import android.util.PrintWriterPrinter; import android.util.Printer; +import android.view.Gravity; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -679,7 +680,7 @@ public class InputMethodService extends AbstractInputMethodService { mInflater = (LayoutInflater)getSystemService( Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState, - false); + WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false); if (mHardwareAccelerated) { mWindow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); } diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java index a9bace1..795117e 100644 --- a/core/java/android/inputmethodservice/SoftInputWindow.java +++ b/core/java/android/inputmethodservice/SoftInputWindow.java @@ -37,6 +37,8 @@ public class SoftInputWindow extends Dialog { final Callback mCallback; final KeyEvent.Callback mKeyEventCallback; final KeyEvent.DispatcherState mDispatcherState; + final int mWindowType; + final int mGravity; final boolean mTakesFocus; private final Rect mBounds = new Rect(); @@ -64,12 +66,14 @@ public class SoftInputWindow extends Dialog { */ public SoftInputWindow(Context context, String name, int theme, Callback callback, KeyEvent.Callback keyEventCallback, KeyEvent.DispatcherState dispatcherState, - boolean takesFocus) { + int windowType, int gravity, boolean takesFocus) { super(context, theme); mName = name; mCallback = callback; mKeyEventCallback = keyEventCallback; mDispatcherState = dispatcherState; + mWindowType = windowType; + mGravity = gravity; mTakesFocus = takesFocus; initDockWindow(); } @@ -97,47 +101,6 @@ public class SoftInputWindow extends Dialog { } /** - * Get the size of the DockWindow. - * - * @return If the DockWindow sticks to the top or bottom of the screen, the - * return value is the height of the DockWindow, and its width is - * equal to the width of the screen; If the DockWindow sticks to the - * left or right of the screen, the return value is the width of the - * DockWindow, and its height is equal to the height of the screen. - */ - public int getSize() { - WindowManager.LayoutParams lp = getWindow().getAttributes(); - - if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { - return lp.height; - } else { - return lp.width; - } - } - - /** - * Set the size of the DockWindow. - * - * @param size If the DockWindow sticks to the top or bottom of the screen, - * <var>size</var> is the height of the DockWindow, and its width is - * equal to the width of the screen; If the DockWindow sticks to the - * left or right of the screen, <var>size</var> is the width of the - * DockWindow, and its height is equal to the height of the screen. - */ - public void setSize(int size) { - WindowManager.LayoutParams lp = getWindow().getAttributes(); - - if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { - lp.width = -1; - lp.height = size; - } else { - lp.width = size; - lp.height = -1; - } - getWindow().setAttributes(lp); - } - - /** * Set which boundary of the screen the DockWindow sticks to. * * @param gravity The boundary of the screen to stick. See {#link @@ -147,18 +110,22 @@ public class SoftInputWindow extends Dialog { */ public void setGravity(int gravity) { WindowManager.LayoutParams lp = getWindow().getAttributes(); - - boolean oldIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); - lp.gravity = gravity; + updateWidthHeight(lp); + getWindow().setAttributes(lp); + } - boolean newIsVertical = (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM); + public int getGravity() { + return getWindow().getAttributes().gravity; + } - if (oldIsVertical != newIsVertical) { - int tmp = lp.width; - lp.width = lp.height; - lp.height = tmp; - getWindow().setAttributes(lp); + private void updateWidthHeight(WindowManager.LayoutParams lp) { + if (lp.gravity == Gravity.TOP || lp.gravity == Gravity.BOTTOM) { + lp.width = WindowManager.LayoutParams.MATCH_PARENT; + lp.height = WindowManager.LayoutParams.WRAP_CONTENT; + } else { + lp.width = WindowManager.LayoutParams.WRAP_CONTENT; + lp.height = WindowManager.LayoutParams.MATCH_PARENT; } } @@ -201,14 +168,11 @@ public class SoftInputWindow extends Dialog { private void initDockWindow() { WindowManager.LayoutParams lp = getWindow().getAttributes(); - lp.type = WindowManager.LayoutParams.TYPE_INPUT_METHOD; + lp.type = mWindowType; lp.setTitle(mName); - lp.gravity = Gravity.BOTTOM; - lp.width = -1; - // Let the input method window's orientation follow sensor based rotation - // Turn this off for now, it is very problematic. - //lp.screenOrientation = ActivityInfo.SCREEN_ORIENTATION_USER; + lp.gravity = mGravity; + updateWidthHeight(lp); getWindow().setAttributes(lp); diff --git a/core/java/android/net/BaseNetworkStateTracker.java b/core/java/android/net/BaseNetworkStateTracker.java index 804f8ee..79db389 100644 --- a/core/java/android/net/BaseNetworkStateTracker.java +++ b/core/java/android/net/BaseNetworkStateTracker.java @@ -44,7 +44,8 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { protected NetworkInfo mNetworkInfo; protected LinkProperties mLinkProperties; - protected LinkCapabilities mLinkCapabilities; + protected NetworkCapabilities mNetworkCapabilities; + protected Network mNetwork = new Network(ConnectivityManager.INVALID_NET_ID); private AtomicBoolean mTeardownRequested = new AtomicBoolean(false); private AtomicBoolean mPrivateDnsRouteSet = new AtomicBoolean(false); @@ -54,7 +55,7 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { mNetworkInfo = new NetworkInfo( networkType, -1, ConnectivityManager.getNetworkTypeName(networkType), null); mLinkProperties = new LinkProperties(); - mLinkCapabilities = new LinkCapabilities(); + mNetworkCapabilities = new NetworkCapabilities(); } protected BaseNetworkStateTracker() { @@ -98,8 +99,8 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { } @Override - public LinkCapabilities getLinkCapabilities() { - return new LinkCapabilities(mLinkCapabilities); + public NetworkCapabilities getNetworkCapabilities() { + return new NetworkCapabilities(mNetworkCapabilities); } @Override @@ -201,4 +202,14 @@ public abstract class BaseNetworkStateTracker implements NetworkStateTracker { public void stopSampling(SamplingDataTracker.SamplingSnapshot s) { // nothing to do } + + @Override + public void setNetId(int netId) { + mNetwork = new Network(netId); + } + + @Override + public Network getNetwork() { + return mNetwork; + } } diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 30d7043..a48a388 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -13,26 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package android.net; import static com.android.internal.util.Preconditions.checkNotNull; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.os.Binder; import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.os.INetworkActivityListener; import android.os.INetworkManagementService; +import android.os.Looper; +import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.os.ServiceManager; import android.provider.Settings; +import android.telephony.TelephonyManager; import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.telephony.ITelephony; +import com.android.internal.telephony.PhoneConstants; +import com.android.internal.util.Protocol; import java.net.InetAddress; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.HashMap; /** * Class that answers queries about the state of network connectivity. It also @@ -48,13 +61,15 @@ import java.net.InetAddress; * is lost</li> * <li>Provide an API that allows applications to query the coarse-grained or fine-grained * state of the available networks</li> + * <li>Provide an API that allows applications to request and select networks for their data + * traffic</li> * </ol> */ public class ConnectivityManager { private static final String TAG = "ConnectivityManager"; /** - * A change in network connectivity has occurred. A connection has either + * A change in network connectivity has occurred. A default connection has either * been established or lost. The NetworkInfo for the affected network is * sent as an extra; it should be consulted to see what kind of * connectivity event occurred. @@ -408,6 +423,11 @@ public class ConnectivityManager { */ public static final int CONNECTIVITY_CHANGE_DELAY_DEFAULT = 3000; + /** + * @hide + */ + public final static int INVALID_NET_ID = 0; + private final IConnectivityManager mService; private final String mPackageName; @@ -529,38 +549,32 @@ public class ConnectivityManager { /** * Specifies the preferred network type. When the device has more * than one type available the preferred network type will be used. - * Note that this made sense when we only had 2 network types, - * but with more and more default networks we need an array to list - * their ordering. This will be deprecated soon. * * @param preference the network type to prefer over all others. It is * unspecified what happens to the old preferred network in the * overall ordering. + * @deprecated Functionality has been removed as it no longer makes sense, + * with many more than two networks - we'd need an array to express + * preference. Instead we use dynamic network properties of + * the networks to describe their precedence. */ public void setNetworkPreference(int preference) { - try { - mService.setNetworkPreference(preference); - } catch (RemoteException e) { - } } /** * Retrieves the current preferred network type. - * Note that this made sense when we only had 2 network types, - * but with more and more default networks we need an array to list - * their ordering. This will be deprecated soon. * * @return an integer representing the preferred network type * * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * @deprecated Functionality has been removed as it no longer makes sense, + * with many more than two networks - we'd need an array to express + * preference. Instead we use dynamic network properties of + * the networks to describe their precedence. */ public int getNetworkPreference() { - try { - return mService.getNetworkPreference(); - } catch (RemoteException e) { - return -1; - } + return TYPE_NONE; } /** @@ -700,7 +714,37 @@ public class ConnectivityManager { */ public LinkProperties getLinkProperties(int networkType) { try { - return mService.getLinkProperties(networkType); + return mService.getLinkPropertiesForType(networkType); + } catch (RemoteException e) { + return null; + } + } + + /** + * Get the {@link LinkProperties} for the given {@link Network}. This + * will return {@code null} if the network is unknown. + * + * @param network The {@link Network} object identifying the network in question. + * @return The {@link LinkProperties} for the network, or {@code null}. + **/ + public LinkProperties getLinkProperties(Network network) { + try { + return mService.getLinkProperties(network); + } catch (RemoteException e) { + return null; + } + } + + /** + * Get the {@link NetworkCapabilities} for the given {@link Network}. This + * will return {@code null} if the network is unknown. + * + * @param network The {@link Network} object identifying the network in question. + * @return The {@link NetworkCapabilities} for the network, or {@code null}. + */ + public NetworkCapabilities getNetworkCapabilities(Network network) { + try { + return mService.getNetworkCapabilities(network); } catch (RemoteException e) { return null; } @@ -718,13 +762,14 @@ public class ConnectivityManager { * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ - public boolean setRadios(boolean turnOn) { - try { - return mService.setRadios(turnOn); - } catch (RemoteException e) { - return false; - } - } +// TODO - check for any callers and remove +// public boolean setRadios(boolean turnOn) { +// try { +// return mService.setRadios(turnOn); +// } catch (RemoteException e) { +// return false; +// } +// } /** * Tells a given networkType to set its radio power state as directed. @@ -738,13 +783,14 @@ public class ConnectivityManager { * {@link android.Manifest.permission#CHANGE_NETWORK_STATE}. * {@hide} */ - public boolean setRadio(int networkType, boolean turnOn) { - try { - return mService.setRadio(networkType, turnOn); - } catch (RemoteException e) { - return false; - } - } +// TODO - check for any callers and remove +// public boolean setRadio(int networkType, boolean turnOn) { +// try { +// return mService.setRadio(networkType, turnOn); +// } catch (RemoteException e) { +// return false; +// } +// } /** * Tells the underlying networking system that the caller wants to @@ -758,13 +804,38 @@ public class ConnectivityManager { * The interpretation of this value is specific to each networking * implementation+feature combination, except that the value {@code -1} * always indicates failure. + * + * @deprecated Deprecated in favor of the cleaner {@link #requestNetwork} api. */ public int startUsingNetworkFeature(int networkType, String feature) { - try { - return mService.startUsingNetworkFeature(networkType, feature, - new Binder()); - } catch (RemoteException e) { - return -1; + NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature); + if (netCap == null) { + Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " + + feature); + return PhoneConstants.APN_REQUEST_FAILED; + } + + NetworkRequest request = null; + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l != null) { + Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest); + renewRequestLocked(l); + if (l.currentNetwork != null) { + return PhoneConstants.APN_ALREADY_ACTIVE; + } else { + return PhoneConstants.APN_REQUEST_STARTED; + } + } + + request = requestNetworkForFeatureLocked(netCap); + } + if (request != null) { + Log.d(TAG, "starting startUsingNeworkFeature for request " + request); + return PhoneConstants.APN_REQUEST_STARTED; + } else { + Log.d(TAG, " request Failed"); + return PhoneConstants.APN_REQUEST_FAILED; } } @@ -780,13 +851,176 @@ public class ConnectivityManager { * The interpretation of this value is specific to each networking * implementation+feature combination, except that the value {@code -1} * always indicates failure. + * + * @deprecated Deprecated in favor of the cleaner {@link #requestNetwork} api. */ public int stopUsingNetworkFeature(int networkType, String feature) { - try { - return mService.stopUsingNetworkFeature(networkType, feature); - } catch (RemoteException e) { + NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature); + if (netCap == null) { + Log.d(TAG, "Can't satisfy stopUsingNetworkFeature for " + networkType + ", " + + feature); return -1; } + + NetworkRequest request = removeRequestForFeature(netCap); + if (request != null) { + Log.d(TAG, "stopUsingNetworkFeature for " + networkType + ", " + feature); + releaseNetworkRequest(request); + } + return 1; + } + + private NetworkCapabilities networkCapabilitiesForFeature(int networkType, String feature) { + if (networkType == TYPE_MOBILE) { + int cap = -1; + if ("enableMMS".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_MMS; + } else if ("enableSUPL".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_SUPL; + } else if ("enableDUN".equals(feature) || "enableDUNAlways".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_DUN; + } else if ("enableHIPRI".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_INTERNET; + } else if ("enableFOTA".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_FOTA; + } else if ("enableIMS".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_IMS; + } else if ("enableCBS".equals(feature)) { + cap = NetworkCapabilities.NET_CAPABILITY_CBS; + } else { + return null; + } + NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR); + netCap.addNetworkCapability(cap); + return netCap; + } else if (networkType == TYPE_WIFI) { + if ("p2p".equals(feature)) { + NetworkCapabilities netCap = new NetworkCapabilities(); + netCap.addTransportType(NetworkCapabilities.TRANSPORT_WIFI); + netCap.addNetworkCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P); + return netCap; + } + } + return null; + } + + private int legacyTypeForNetworkCapabilities(NetworkCapabilities netCap) { + if (netCap == null) return TYPE_NONE; + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) { + return TYPE_MOBILE_CBS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) { + return TYPE_MOBILE_IMS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) { + return TYPE_MOBILE_FOTA; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) { + return TYPE_MOBILE_DUN; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) { + return TYPE_MOBILE_SUPL; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) { + return TYPE_MOBILE_MMS; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + return TYPE_MOBILE_HIPRI; + } + if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P)) { + return TYPE_WIFI_P2P; + } + return TYPE_NONE; + } + + private static class LegacyRequest { + NetworkCapabilities networkCapabilities; + NetworkRequest networkRequest; + int expireSequenceNumber; + Network currentNetwork; + int delay = -1; + NetworkCallbackListener networkCallbackListener = new NetworkCallbackListener() { + @Override + public void onAvailable(NetworkRequest request, Network network) { + currentNetwork = network; + Log.d(TAG, "startUsingNetworkFeature got Network:" + network); + network.bindProcessForHostResolution(); + } + @Override + public void onLost(NetworkRequest request, Network network) { + if (network.equals(currentNetwork)) { + currentNetwork = null; + network.unbindProcessForHostResolution(); + } + Log.d(TAG, "startUsingNetworkFeature lost Network:" + network); + } + }; + } + + private HashMap<NetworkCapabilities, LegacyRequest> sLegacyRequests = + new HashMap<NetworkCapabilities, LegacyRequest>(); + + private NetworkRequest findRequestForFeature(NetworkCapabilities netCap) { + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l != null) return l.networkRequest; + } + return null; + } + + private void renewRequestLocked(LegacyRequest l) { + l.expireSequenceNumber++; + Log.d(TAG, "renewing request to seqNum " + l.expireSequenceNumber); + sendExpireMsgForFeature(l.networkCapabilities, l.expireSequenceNumber, l.delay); + } + + private void expireRequest(NetworkCapabilities netCap, int sequenceNum) { + int ourSeqNum = -1; + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.get(netCap); + if (l == null) return; + ourSeqNum = l.expireSequenceNumber; + if (l.expireSequenceNumber == sequenceNum) { + releaseNetworkRequest(l.networkRequest); + sLegacyRequests.remove(netCap); + } + } + Log.d(TAG, "expireRequest with " + ourSeqNum + ", " + sequenceNum); + } + + private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) { + int delay = -1; + int type = legacyTypeForNetworkCapabilities(netCap); + try { + delay = mService.getRestoreDefaultNetworkDelay(type); + } catch (RemoteException e) {} + LegacyRequest l = new LegacyRequest(); + l.networkCapabilities = netCap; + l.delay = delay; + l.expireSequenceNumber = 0; + l.networkRequest = sendRequestForNetwork(netCap, l.networkCallbackListener, 0, + REQUEST, type); + if (l.networkRequest == null) return null; + sLegacyRequests.put(netCap, l); + sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay); + return l.networkRequest; + } + + private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) { + if (delay >= 0) { + Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay); + Message msg = sCallbackHandler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap); + sCallbackHandler.sendMessageDelayed(msg, delay); + } + } + + private NetworkRequest removeRequestForFeature(NetworkCapabilities netCap) { + synchronized (sLegacyRequests) { + LegacyRequest l = sLegacyRequests.remove(netCap); + if (l == null) return null; + return l.networkRequest; + } } /** @@ -799,6 +1033,9 @@ public class ConnectivityManager { * host is to be routed * @param hostAddress the IP address of the host to which the route is desired * @return {@code true} on success, {@code false} on failure + * + * @deprecated Deprecated in favor of the {@link #requestNetwork}, + * {@link Network#bindProcess} and {@link Network#socketFactory} api. */ public boolean requestRouteToHost(int networkType, int hostAddress) { InetAddress inetAddress = NetworkUtils.intToInetAddress(hostAddress); @@ -821,6 +1058,8 @@ public class ConnectivityManager { * @param hostAddress the IP address of the host to which the route is desired * @return {@code true} on success, {@code false} on failure * @hide + * @deprecated Deprecated in favor of the {@link #requestNetwork} and + * {@link Network#bindProcess} api. */ public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) { byte[] address = hostAddress.getAddress(); @@ -888,34 +1127,18 @@ public class ConnectivityManager { } /** - * Gets the value of the setting for enabling Mobile data. - * - * @return Whether mobile data is enabled. - * - * <p>This method requires the call to hold the permission - * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * @hide + * @deprecated Talk to TelephonyManager directly */ public boolean getMobileDataEnabled() { - try { - return mService.getMobileDataEnabled(); - } catch (RemoteException e) { - return true; - } - } - - /** - * Sets the persisted value for enabling/disabling Mobile data. - * - * @param enabled Whether the user wants the mobile data connection used - * or not. - * @hide - */ - public void setMobileDataEnabled(boolean enabled) { - try { - mService.setMobileDataEnabled(enabled); - } catch (RemoteException e) { + IBinder b = ServiceManager.getService(Context.TELEPHONY_SERVICE); + if (b != null) { + try { + ITelephony it = ITelephony.Stub.asInterface(b); + return it.getDataEnabled(); + } catch (RemoteException e) { } } + return false; } /** @@ -1302,6 +1525,22 @@ public class ConnectivityManager { } /** + * Report a problem network to the framework. This provides a hint to the system + * that there might be connectivity problems on this network and may cause + * the framework to re-evaluate network connectivity and/or switch to another + * network. + * + * @param network The {@link Network} the application was attempting to use + * or {@code null} to indicate the current default network. + */ + public void reportBadNetwork(Network network) { + try { + mService.reportBadNetwork(network); + } catch (RemoteException e) { + } + } + + /** * Set a network-independent global http proxy. This is not normally what you want * for typical HTTP proxies - they are general network dependent. However if you're * doing something unusual like general internal filtering this may be useful. On @@ -1312,6 +1551,7 @@ public class ConnectivityManager { * * <p>This method requires the call to hold the permission * android.Manifest.permission#CONNECTIVITY_INTERNAL. + * @hide */ public void setGlobalProxy(ProxyInfo p) { try { @@ -1328,6 +1568,7 @@ public class ConnectivityManager { * * <p>This method requires the call to hold the permission * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. + * @hide */ public ProxyInfo getGlobalProxy() { try { @@ -1347,6 +1588,7 @@ public class ConnectivityManager { * <p>This method requires the call to hold the permission * {@link android.Manifest.permission#ACCESS_NETWORK_STATE}. * {@hide} + * @deprecated Deprecated in favor of {@link #getLinkProperties} */ public ProxyInfo getProxy() { try { @@ -1582,4 +1824,491 @@ public class ConnectivityManager { } catch (RemoteException e) { } } + + /** {@hide} */ + public void registerNetworkFactory(Messenger messenger, String name) { + try { + mService.registerNetworkFactory(messenger, name); + } catch (RemoteException e) { } + } + + /** {@hide} */ + public void unregisterNetworkFactory(Messenger messenger) { + try { + mService.unregisterNetworkFactory(messenger); + } catch (RemoteException e) { } + } + + /** {@hide} */ + public void registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp, + NetworkCapabilities nc, int score) { + try { + mService.registerNetworkAgent(messenger, ni, lp, nc, score); + } catch (RemoteException e) { } + } + + /** + * Base class for NetworkRequest callbacks. Used for notifications about network + * changes. Should be extended by applications wanting notifications. + */ + public static class NetworkCallbackListener { + /** @hide */ + public static final int PRECHECK = 1; + /** @hide */ + public static final int AVAILABLE = 2; + /** @hide */ + public static final int LOSING = 3; + /** @hide */ + public static final int LOST = 4; + /** @hide */ + public static final int UNAVAIL = 5; + /** @hide */ + public static final int CAP_CHANGED = 6; + /** @hide */ + public static final int PROP_CHANGED = 7; + /** @hide */ + public static final int CANCELED = 8; + + /** + * @hide + * Called whenever the framework connects to a network that it may use to + * satisfy this request + */ + public void onPreCheck(NetworkRequest networkRequest, Network network) {} + + /** + * Called when the framework connects and has declared new network ready for use. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + * @param network The {@link Network} of the satisfying network. + */ + public void onAvailable(NetworkRequest networkRequest, Network network) {} + + /** + * Called when the network is about to be disconnected. Often paired with an + * {@link NetworkCallbackListener#onAvailable} call with the new replacement network + * for graceful handover. This may not be called if we have a hard loss + * (loss without warning). This may be followed by either a + * {@link NetworkCallbackListener#onLost} call or a + * {@link NetworkCallbackListener#onAvailable} call for this network depending + * on whether we lose or regain it. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + * @param network The {@link Network} of the failing network. + * @param maxSecToLive The time in seconds the framework will attempt to keep the + * network connected. Note that the network may suffers a + * hard loss at any time. + */ + public void onLosing(NetworkRequest networkRequest, Network network, int maxSecToLive) {} + + /** + * Called when the framework has a hard loss of the network or when the + * graceful failure ends. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + * @param network The {@link Network} lost. + */ + public void onLost(NetworkRequest networkRequest, Network network) {} + + /** + * Called if no network is found in the given timeout time. If no timeout is given, + * this will not be called. + * @hide + */ + public void onUnavailable(NetworkRequest networkRequest) {} + + /** + * Called when the network the framework connected to for this request + * changes capabilities but still satisfies the stated need. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + * @param network The {@link Network} whose capabilities have changed. + * @param networkCapabilities The new {@link NetworkCapabilities} for this network. + */ + public void onNetworkCapabilitiesChanged(NetworkRequest networkRequest, Network network, + NetworkCapabilities networkCapabilities) {} + + /** + * Called when the network the framework connected to for this request + * changes {@link LinkProperties}. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + * @param network The {@link Network} whose link properties have changed. + * @param linkProperties The new {@link LinkProperties} for this network. + */ + public void onLinkPropertiesChanged(NetworkRequest networkRequest, Network network, + LinkProperties linkProperties) {} + + /** + * Called when a {@link #releaseNetworkRequest} call concludes and the registered + * callbacks will no longer be used. + * + * @param networkRequest The {@link NetworkRequest} used to initiate the request. + */ + public void onReleased(NetworkRequest networkRequest) {} + } + + private static final int BASE = Protocol.BASE_CONNECTIVITY_MANAGER; + /** @hide obj = pair(NetworkRequest, Network) */ + public static final int CALLBACK_PRECHECK = BASE + 1; + /** @hide obj = pair(NetworkRequest, Network) */ + public static final int CALLBACK_AVAILABLE = BASE + 2; + /** @hide obj = pair(NetworkRequest, Network), arg1 = ttl */ + public static final int CALLBACK_LOSING = BASE + 3; + /** @hide obj = pair(NetworkRequest, Network) */ + public static final int CALLBACK_LOST = BASE + 4; + /** @hide obj = NetworkRequest */ + public static final int CALLBACK_UNAVAIL = BASE + 5; + /** @hide obj = pair(NetworkRequest, Network) */ + public static final int CALLBACK_CAP_CHANGED = BASE + 6; + /** @hide obj = pair(NetworkRequest, Network) */ + public static final int CALLBACK_IP_CHANGED = BASE + 7; + /** @hide obj = NetworkRequest */ + public static final int CALLBACK_RELEASED = BASE + 8; + /** @hide */ + public static final int CALLBACK_EXIT = BASE + 9; + /** @hide obj = NetworkCapabilities, arg1 = seq number */ + private static final int EXPIRE_LEGACY_REQUEST = BASE + 10; + + private class CallbackHandler extends Handler { + private final HashMap<NetworkRequest, NetworkCallbackListener>mCallbackMap; + private final AtomicInteger mRefCount; + private static final String TAG = "ConnectivityManager.CallbackHandler"; + private final ConnectivityManager mCm; + + CallbackHandler(Looper looper, HashMap<NetworkRequest, NetworkCallbackListener>callbackMap, + AtomicInteger refCount, ConnectivityManager cm) { + super(looper); + mCallbackMap = callbackMap; + mRefCount = refCount; + mCm = cm; + } + + @Override + public void handleMessage(Message message) { + Log.d(TAG, "CM callback handler got msg " + message.what); + switch (message.what) { + case CALLBACK_PRECHECK: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + callbacks.onPreCheck(request, getNetwork(message)); + } else { + Log.e(TAG, "callback not found for PRECHECK message"); + } + break; + } + case CALLBACK_AVAILABLE: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + callbacks.onAvailable(request, getNetwork(message)); + } else { + Log.e(TAG, "callback not found for AVAILABLE message"); + } + break; + } + case CALLBACK_LOSING: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + callbacks.onLosing(request, getNetwork(message), message.arg1); + } else { + Log.e(TAG, "callback not found for LOSING message"); + } + break; + } + case CALLBACK_LOST: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + callbacks.onLost(request, getNetwork(message)); + } else { + Log.e(TAG, "callback not found for LOST message"); + } + break; + } + case CALLBACK_UNAVAIL: { + NetworkRequest req = (NetworkRequest)message.obj; + NetworkCallbackListener callbacks = null; + synchronized(mCallbackMap) { + callbacks = mCallbackMap.get(req); + } + if (callbacks != null) { + callbacks.onUnavailable(req); + } else { + Log.e(TAG, "callback not found for UNAVAIL message"); + } + break; + } + case CALLBACK_CAP_CHANGED: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + Network network = getNetwork(message); + NetworkCapabilities cap = mCm.getNetworkCapabilities(network); + + callbacks.onNetworkCapabilitiesChanged(request, network, cap); + } else { + Log.e(TAG, "callback not found for CHANGED message"); + } + break; + } + case CALLBACK_IP_CHANGED: { + NetworkRequest request = getNetworkRequest(message); + NetworkCallbackListener callbacks = getCallbacks(request); + if (callbacks != null) { + Network network = getNetwork(message); + LinkProperties lp = mCm.getLinkProperties(network); + + callbacks.onLinkPropertiesChanged(request, network, lp); + } else { + Log.e(TAG, "callback not found for CHANGED message"); + } + break; + } + case CALLBACK_RELEASED: { + NetworkRequest req = (NetworkRequest)message.obj; + NetworkCallbackListener callbacks = null; + synchronized(mCallbackMap) { + callbacks = mCallbackMap.remove(req); + } + if (callbacks != null) { + callbacks.onReleased(req); + } else { + Log.e(TAG, "callback not found for CANCELED message"); + } + synchronized(mRefCount) { + if (mRefCount.decrementAndGet() == 0) { + getLooper().quit(); + } + } + break; + } + case CALLBACK_EXIT: { + Log.d(TAG, "Listener quiting"); + getLooper().quit(); + break; + } + case EXPIRE_LEGACY_REQUEST: { + expireRequest((NetworkCapabilities)message.obj, message.arg1); + break; + } + } + } + + private NetworkRequest getNetworkRequest(Message msg) { + return (NetworkRequest)(msg.obj); + } + private NetworkCallbackListener getCallbacks(NetworkRequest req) { + synchronized(mCallbackMap) { + return mCallbackMap.get(req); + } + } + private Network getNetwork(Message msg) { + return new Network(msg.arg2); + } + private NetworkCallbackListener removeCallbacks(Message msg) { + NetworkRequest req = (NetworkRequest)msg.obj; + synchronized(mCallbackMap) { + return mCallbackMap.remove(req); + } + } + } + + private void addCallbackListener() { + synchronized(sCallbackRefCount) { + if (sCallbackRefCount.incrementAndGet() == 1) { + // TODO - switch this over to a ManagerThread or expire it when done + HandlerThread callbackThread = new HandlerThread("ConnectivityManager"); + callbackThread.start(); + sCallbackHandler = new CallbackHandler(callbackThread.getLooper(), + sNetworkCallbackListener, sCallbackRefCount, this); + } + } + } + + private void removeCallbackListener() { + synchronized(sCallbackRefCount) { + if (sCallbackRefCount.decrementAndGet() == 0) { + sCallbackHandler.obtainMessage(CALLBACK_EXIT).sendToTarget(); + sCallbackHandler = null; + } + } + } + + static final HashMap<NetworkRequest, NetworkCallbackListener> sNetworkCallbackListener = + new HashMap<NetworkRequest, NetworkCallbackListener>(); + static final AtomicInteger sCallbackRefCount = new AtomicInteger(0); + static CallbackHandler sCallbackHandler = null; + + private final static int LISTEN = 1; + private final static int REQUEST = 2; + + private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, + NetworkCallbackListener networkCallbackListener, int timeoutSec, int action, + int legacyType) { + NetworkRequest networkRequest = null; + if (networkCallbackListener == null) { + throw new IllegalArgumentException("null NetworkCallbackListener"); + } + if (need == null) throw new IllegalArgumentException("null NetworkCapabilities"); + try { + addCallbackListener(); + if (action == LISTEN) { + networkRequest = mService.listenForNetwork(need, new Messenger(sCallbackHandler), + new Binder()); + } else { + networkRequest = mService.requestNetwork(need, new Messenger(sCallbackHandler), + timeoutSec, new Binder(), legacyType); + } + if (networkRequest != null) { + synchronized(sNetworkCallbackListener) { + sNetworkCallbackListener.put(networkRequest, networkCallbackListener); + } + } + } catch (RemoteException e) {} + if (networkRequest == null) removeCallbackListener(); + return networkRequest; + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}. + * + * This {@link NetworkRequest} will live until released via + * {@link #releaseNetworkRequest} or the calling application exits. + * Status of the request can be followed by listening to the various + * callbacks described in {@link NetworkCallbackListener}. The {@link Network} + * can be used to direct traffic to the network. + * + * @param need {@link NetworkCapabilities} required by this request. + * @param networkCallbackListener The {@link NetworkCallbackListener} to be utilized for this + * request. Note the callbacks can be shared by multiple + * requests and the NetworkRequest token utilized to + * determine to which request the callback relates. + * @return A {@link NetworkRequest} object identifying the request. + */ + public NetworkRequest requestNetwork(NetworkCapabilities need, + NetworkCallbackListener networkCallbackListener) { + return sendRequestForNetwork(need, networkCallbackListener, 0, REQUEST, TYPE_NONE); + } + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}, limited + * by a timeout. + * + * This function behaves identically to the non-timedout version, but if a suitable + * network is not found within the given time (in Seconds) the + * {@link NetworkCallbackListener#unavailable} callback is called. The request must + * still be released normally by calling {@link releaseNetworkRequest}. + * @param need {@link NetworkCapabilities} required by this request. + * @param networkCallbackListener The callbacks to be utilized for this request. Note + * the callbacks can be shared by multiple requests and + * the NetworkRequest token utilized to determine to which + * request the callback relates. + * @param timeoutSec The time in seconds to attempt looking for a suitable network + * before {@link NetworkCallbackListener#unavailable} is called. + * @return A {@link NetworkRequest} object identifying the request. + * @hide + */ + public NetworkRequest requestNetwork(NetworkCapabilities need, + NetworkCallbackListener networkCallbackListener, int timeoutSec) { + return sendRequestForNetwork(need, networkCallbackListener, timeoutSec, REQUEST, + TYPE_NONE); + } + + /** + * The maximum number of seconds the framework will look for a suitable network + * during a timeout-equiped call to {@link requestNetwork}. + * {@hide} + */ + public final static int MAX_NETWORK_REQUEST_TIMEOUT_SEC = 100 * 60; + + /** + * The lookup key for a {@link Network} object included with the intent after + * succesfully finding a network for the applications request. Retrieve it with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_NETWORK_REQUEST_NETWORK = "networkRequestNetwork"; + + /** + * The lookup key for a {@link NetworkCapabilities} object included with the intent after + * succesfully finding a network for the applications request. Retrieve it with + * {@link android.content.Intent#getParcelableExtra(String)}. + */ + public static final String EXTRA_NETWORK_REQUEST_NETWORK_CAPABILITIES = + "networkRequestNetworkCapabilities"; + + + /** + * Request a network to satisfy a set of {@link NetworkCapabilities}. + * + * This function behavies identically to the callback-equiped version, but instead + * of {@link NetworkCallbackListener} a {@link PendingIntent} is used. This means + * the request may outlive the calling application and get called back when a suitable + * network is found. + * <p> + * The operation is an Intent broadcast that goes to a broadcast receiver that + * you registered with {@link Context#registerReceiver} or through the + * <receiver> tag in an AndroidManifest.xml file + * <p> + * The operation Intent is delivered with two extras, a {@link Network} typed + * extra called {@link #EXTRA_NETWORK_REQUEST_NETWORK} and a {@link NetworkCapabilities} + * typed extra called {@link #EXTRA_NETWORK_REQUEST_NETWORK_CAPABILITIES} containing + * the original requests parameters. It is important to create a new, + * {@link NetworkCallbackListener} based request before completing the processing of the + * Intent to reserve the network or it will be released shortly after the Intent + * is processed. + * <p> + * If there is already an request for this Intent registered (with the equality of + * two Intents defined by {@link Intent#filterEquals}), then it will be removed and + * replaced by this one, effectively releasing the previous {@link NetworkRequest}. + * <p> + * The request may be released normally by calling {@link #releaseNetworkRequest}. + * + * @param need {@link NetworkCapabilities} required by this request. + * @param operation Action to perform when the network is available (corresponds + * to the {@link NetworkCallbackListener#onAvailable} call. Typically + * comes from {@link PendingIntent#getBroadcast}. + * @return A {@link NetworkRequest} object identifying the request. + */ + public NetworkRequest requestNetwork(NetworkCapabilities need, PendingIntent operation) { + try { + return mService.pendingRequestForNetwork(need, operation); + } catch (RemoteException e) {} + return null; + } + + /** + * Registers to receive notifications about all networks which satisfy the given + * {@link NetworkCapabilities}. The callbacks will continue to be called until + * either the application exits or the request is released using + * {@link #releaseNetworkRequest}. + * + * @param need {@link NetworkCapabilities} required by this request. + * @param networkCallbackListener The {@link NetworkCallbackListener} to be called as suitable + * networks change state. + * @return A {@link NetworkRequest} object identifying the request. + */ + public NetworkRequest listenForNetwork(NetworkCapabilities need, + NetworkCallbackListener networkCallbackListener) { + return sendRequestForNetwork(need, networkCallbackListener, 0, LISTEN, TYPE_NONE); + } + + /** + * Releases a {@link NetworkRequest} generated either through a {@link #requestNetwork} + * or a {@link #listenForNetwork} call. The {@link NetworkCallbackListener} given in the + * earlier call may continue receiving calls until the + * {@link NetworkCallbackListener#onReleased} function is called, signifying the end + * of the request. + * + * @param networkRequest The {@link NetworkRequest} generated by an earlier call to + * {@link #requestNetwork} or {@link #listenForNetwork}. + */ + public void releaseNetworkRequest(NetworkRequest networkRequest) { + if (networkRequest == null) throw new IllegalArgumentException("null NetworkRequest"); + try { + mService.releaseNetworkRequest(networkRequest); + } catch (RemoteException e) {} + } } diff --git a/core/java/android/net/DummyDataStateTracker.java b/core/java/android/net/DummyDataStateTracker.java index a5d059e..eff9f9f 100644 --- a/core/java/android/net/DummyDataStateTracker.java +++ b/core/java/android/net/DummyDataStateTracker.java @@ -190,13 +190,6 @@ public class DummyDataStateTracker extends BaseNetworkStateTracker { return new LinkProperties(mLinkProperties); } - /** - * @see android.net.NetworkStateTracker#getLinkCapabilities() - */ - public LinkCapabilities getLinkCapabilities() { - return new LinkCapabilities(mLinkCapabilities); - } - public void setDependencyMet(boolean met) { // not supported on this network } diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java deleted file mode 100644 index 10b5d0b..0000000 --- a/core/java/android/net/EthernetDataTracker.java +++ /dev/null @@ -1,437 +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.net; - -import android.content.Context; -import android.net.NetworkInfo.DetailedState; -import android.os.Handler; -import android.os.IBinder; -import android.os.INetworkManagementService; -import android.os.Message; -import android.os.Messenger; -import android.os.RemoteException; -import android.os.ServiceManager; -import android.util.Log; - -import com.android.server.net.BaseNetworkObserver; - -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * This class tracks the data connection associated with Ethernet - * This is a singleton class and an instance will be created by - * ConnectivityService. - * @hide - */ -public class EthernetDataTracker extends BaseNetworkStateTracker { - private static final String NETWORKTYPE = "ETHERNET"; - private static final String TAG = "Ethernet"; - - private AtomicBoolean mTeardownRequested = new AtomicBoolean(false); - private AtomicBoolean mPrivateDnsRouteSet = new AtomicBoolean(false); - private AtomicInteger mDefaultGatewayAddr = new AtomicInteger(0); - private AtomicBoolean mDefaultRouteSet = new AtomicBoolean(false); - - private static boolean mLinkUp; - private InterfaceObserver mInterfaceObserver; - private String mHwAddr; - - /* For sending events to connectivity service handler */ - private Handler mCsHandler; - - private static EthernetDataTracker sInstance; - private static String sIfaceMatch = ""; - private static String mIface = ""; - - private INetworkManagementService mNMService; - - private static class InterfaceObserver extends BaseNetworkObserver { - private EthernetDataTracker mTracker; - - InterfaceObserver(EthernetDataTracker tracker) { - super(); - mTracker = tracker; - } - - @Override - public void interfaceStatusChanged(String iface, boolean up) { - Log.d(TAG, "Interface status changed: " + iface + (up ? "up" : "down")); - } - - @Override - public void interfaceLinkStateChanged(String iface, boolean up) { - if (mIface.equals(iface)) { - Log.d(TAG, "Interface " + iface + " link " + (up ? "up" : "down")); - mLinkUp = up; - mTracker.mNetworkInfo.setIsAvailable(up); - - // use DHCP - if (up) { - mTracker.reconnect(); - } else { - mTracker.disconnect(); - } - } - } - - @Override - public void interfaceAdded(String iface) { - mTracker.interfaceAdded(iface); - } - - @Override - public void interfaceRemoved(String iface) { - mTracker.interfaceRemoved(iface); - } - } - - private EthernetDataTracker() { - mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_ETHERNET, 0, NETWORKTYPE, ""); - mLinkProperties = new LinkProperties(); - 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; - - Log.d(TAG, "Adding " + iface); - - synchronized(this) { - if(!mIface.isEmpty()) - return; - mIface = iface; - } - - interfaceUpdated(); - - mNetworkInfo.setIsAvailable(true); - Message msg = mCsHandler.obtainMessage(EVENT_CONFIGURATION_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - - public void disconnect() { - - NetworkUtils.stopDhcp(mIface); - - mLinkProperties.clear(); - mNetworkInfo.setIsAvailable(false); - mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null, mHwAddr); - - Message msg = mCsHandler.obtainMessage(EVENT_CONFIGURATION_CHANGED, mNetworkInfo); - msg.sendToTarget(); - - msg = mCsHandler.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); - msg.sendToTarget(); - - IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); - INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); - try { - service.clearInterfaceAddresses(mIface); - } catch (Exception e) { - Log.e(TAG, "Failed to clear addresses or disable ipv6" + e); - } - } - - private void interfaceRemoved(String iface) { - if (!iface.equals(mIface)) - return; - - Log.d(TAG, "Removing " + iface); - disconnect(); - 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; - - mNetworkInfo.setIsAvailable(true); - mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddr); - Message msg = mCsHandler.obtainMessage(EVENT_STATE_CHANGED, mNetworkInfo); - msg.sendToTarget(); - } - }); - dhcpThread.start(); - } - - public static synchronized EthernetDataTracker getInstance() { - if (sInstance == null) sInstance = new EthernetDataTracker(); - return sInstance; - } - - public Object Clone() throws CloneNotSupportedException { - throw new CloneNotSupportedException(); - } - - public void setTeardownRequested(boolean isRequested) { - mTeardownRequested.set(isRequested); - } - - public boolean isTeardownRequested() { - return mTeardownRequested.get(); - } - - /** - * Begin monitoring connectivity - */ - public void startMonitoring(Context context, Handler target) { - mContext = context; - mCsHandler = target; - - // register for notifications from NetworkManagement Service - IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); - mNMService = INetworkManagementService.Stub.asInterface(b); - - mInterfaceObserver = new InterfaceObserver(this); - - // enable and try to connect to an ethernet interface that - // already exists - sIfaceMatch = context.getResources().getString( - com.android.internal.R.string.config_ethernet_iface_regex); - try { - final String[] ifaces = mNMService.listInterfaces(); - for (String iface : ifaces) { - if (iface.matches(sIfaceMatch)) { - mIface = iface; - interfaceUpdated(); - - // if a DHCP client had previously been started for this interface, then stop it - NetworkUtils.stopDhcp(mIface); - - reconnect(); - break; - } - } - } catch (RemoteException e) { - Log.e(TAG, "Could not get list of interfaces " + e); - } - - try { - mNMService.registerObserver(mInterfaceObserver); - } catch (RemoteException e) { - Log.e(TAG, "Could not register InterfaceObserver " + e); - } - } - - /** - * Disable connectivity to a network - * TODO: do away with return value after making MobileDataStateTracker async - */ - public boolean teardown() { - mTeardownRequested.set(true); - NetworkUtils.stopDhcp(mIface); - return true; - } - - /** - * Re-enable connectivity to a network after a {@link #teardown()}. - */ - public boolean reconnect() { - if (mLinkUp) { - mTeardownRequested.set(false); - runDhcp(); - } - return mLinkUp; - } - - @Override - public void captivePortalCheckCompleted(boolean isCaptivePortal) { - // not implemented - } - - /** - * Turn the wireless radio off for a network. - * @param turnOn {@code true} to turn the radio on, {@code false} - */ - public boolean setRadio(boolean turnOn) { - return true; - } - - /** - * @return true - If are we currently tethered with another device. - */ - public synchronized boolean isAvailable() { - return mNetworkInfo.isAvailable(); - } - - /** - * Tells the underlying networking system that the caller wants to - * begin using the named feature. The interpretation of {@code feature} - * is completely up to each networking implementation. - * @param feature the name of the feature to be used - * @param callingPid the process ID of the process that is issuing this request - * @param callingUid the user ID of the process that is issuing this request - * @return an integer value representing the outcome of the request. - * The interpretation of this value is specific to each networking - * implementation+feature combination, except that the value {@code -1} - * always indicates failure. - * TODO: needs to go away - */ - public int startUsingNetworkFeature(String feature, int callingPid, int callingUid) { - return -1; - } - - /** - * Tells the underlying networking system that the caller is finished - * using the named feature. The interpretation of {@code feature} - * is completely up to each networking implementation. - * @param feature the name of the feature that is no longer needed. - * @param callingPid the process ID of the process that is issuing this request - * @param callingUid the user ID of the process that is issuing this request - * @return an integer value representing the outcome of the request. - * The interpretation of this value is specific to each networking - * implementation+feature combination, except that the value {@code -1} - * always indicates failure. - * TODO: needs to go away - */ - public int stopUsingNetworkFeature(String feature, int callingPid, int callingUid) { - return -1; - } - - @Override - public void setUserDataEnable(boolean enabled) { - Log.w(TAG, "ignoring setUserDataEnable(" + enabled + ")"); - } - - @Override - public void setPolicyDataEnable(boolean enabled) { - Log.w(TAG, "ignoring setPolicyDataEnable(" + enabled + ")"); - } - - /** - * Check if private DNS route is set for the network - */ - public boolean isPrivateDnsRouteSet() { - return mPrivateDnsRouteSet.get(); - } - - /** - * Set a flag indicating private DNS route is set - */ - public void privateDnsRouteSet(boolean enabled) { - mPrivateDnsRouteSet.set(enabled); - } - - /** - * Fetch NetworkInfo for the network - */ - public synchronized NetworkInfo getNetworkInfo() { - return mNetworkInfo; - } - - /** - * Fetch LinkProperties for the network - */ - public synchronized LinkProperties getLinkProperties() { - return new LinkProperties(mLinkProperties); - } - - /** - * A capability is an Integer/String pair, the capabilities - * are defined in the class LinkSocket#Key. - * - * @return a copy of this connections capabilities, may be empty but never null. - */ - public LinkCapabilities getLinkCapabilities() { - return new LinkCapabilities(mLinkCapabilities); - } - - /** - * Fetch default gateway address for the network - */ - public int getDefaultGatewayAddr() { - return mDefaultGatewayAddr.get(); - } - - /** - * Check if default route is set - */ - public boolean isDefaultRouteSet() { - return mDefaultRouteSet.get(); - } - - /** - * Set a flag indicating default route is set for the network - */ - public void defaultRouteSet(boolean enabled) { - mDefaultRouteSet.set(enabled); - } - - /** - * Return the system properties name associated with the tcp buffer sizes - * for this network. - */ - public String getTcpBufferSizesPropName() { - return "net.tcp.buffersize.ethernet"; - } - - public void setDependencyMet(boolean met) { - // not supported on this network - } - - @Override - public void addStackedLink(LinkProperties link) { - mLinkProperties.addStackedLink(link); - } - - @Override - public void removeStackedLink(LinkProperties link) { - mLinkProperties.removeStackedLink(link); - } - - @Override - public void supplyMessenger(Messenger messenger) { - // not supported on this network - } - - @Override - public String getNetworkInterfaceName() { - return mIface; - } -} diff --git a/core/java/android/net/EthernetManager.java b/core/java/android/net/EthernetManager.java new file mode 100644 index 0000000..5df4baf --- /dev/null +++ b/core/java/android/net/EthernetManager.java @@ -0,0 +1,72 @@ +/* + * 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.content.Context; +import android.net.IEthernetManager; +import android.net.IpConfiguration; +import android.net.IpConfiguration.IpAssignment; +import android.net.IpConfiguration.ProxySettings; +import android.net.LinkProperties; +import android.os.RemoteException; + +/** + * A class representing the IP configuration of the Ethernet network. + * + * @hide + */ +public class EthernetManager { + private static final String TAG = "EthernetManager"; + + private final Context mContext; + private final IEthernetManager mService; + + /** + * Create a new EthernetManager instance. + * Applications will almost always want to use + * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve + * the standard {@link android.content.Context#ETHERNET_SERVICE Context.ETHERNET_SERVICE}. + */ + public EthernetManager(Context context, IEthernetManager service) { + mContext = context; + mService = service; + } + + /** + * Get Ethernet configuration. + * @return the Ethernet Configuration, contained in {@link IpConfiguration}. + */ + public IpConfiguration getConfiguration() { + try { + return mService.getConfiguration(); + } catch (RemoteException e) { + return new IpConfiguration(IpAssignment.UNASSIGNED, + ProxySettings.UNASSIGNED, + new LinkProperties()); + } + } + + /** + * Set Ethernet configuration. + */ + public void setConfiguration(IpConfiguration config) { + try { + mService.setConfiguration(config); + } catch (RemoteException e) { + } + } +} diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index d53a856..5f1ff3e 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -16,10 +16,14 @@ package android.net; +import android.app.PendingIntent; import android.net.LinkQualityInfo; import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkInfo; import android.net.NetworkQuotaInfo; +import android.net.NetworkRequest; import android.net.NetworkState; import android.net.ProxyInfo; import android.os.IBinder; @@ -41,10 +45,6 @@ interface IConnectivityManager // Keep this in sync with framework/native/services/connectivitymanager/ConnectivityManager.h void markSocketAsUser(in ParcelFileDescriptor socket, int uid); - void setNetworkPreference(int pref); - - int getNetworkPreference(); - NetworkInfo getActiveNetworkInfo(); NetworkInfo getActiveNetworkInfoForUid(int uid); NetworkInfo getNetworkInfo(int networkType); @@ -55,17 +55,16 @@ interface IConnectivityManager boolean isNetworkSupported(int networkType); LinkProperties getActiveLinkProperties(); - LinkProperties getLinkProperties(int networkType); + LinkProperties getLinkPropertiesForType(int networkType); + LinkProperties getLinkProperties(in Network network); + + NetworkCapabilities getNetworkCapabilities(in Network network); NetworkState[] getAllNetworkState(); NetworkQuotaInfo getActiveNetworkQuotaInfo(); boolean isActiveNetworkMetered(); - boolean setRadios(boolean onOff); - - boolean setRadio(int networkType, boolean turnOn); - int startUsingNetworkFeature(int networkType, in String feature, in IBinder binder); @@ -75,9 +74,6 @@ interface IConnectivityManager boolean requestRouteToHostAddress(int networkType, in byte[] hostAddress, String packageName); - boolean getMobileDataEnabled(); - void setMobileDataEnabled(boolean enabled); - /** Policy control over specific {@link NetworkStateTracker}. */ void setPolicyDataEnable(int networkType, boolean enabled); @@ -107,6 +103,8 @@ interface IConnectivityManager void reportInetCondition(int networkType, int percentage); + void reportBadNetwork(in Network network); + ProxyInfo getGlobalProxy(); void setGlobalProxy(in ProxyInfo p); @@ -147,7 +145,31 @@ interface IConnectivityManager LinkQualityInfo[] getAllLinkQualityInfo(); - void setProvisioningNotificationVisible(boolean visible, int networkType, in String extraInfo, in String url); + void setProvisioningNotificationVisible(boolean visible, int networkType, in String extraInfo, + in String url); void setAirplaneMode(boolean enable); + + void registerNetworkFactory(in Messenger messenger, in String name); + + void unregisterNetworkFactory(in Messenger messenger); + + void registerNetworkAgent(in Messenger messenger, in NetworkInfo ni, in LinkProperties lp, + in NetworkCapabilities nc, int score); + + NetworkRequest requestNetwork(in NetworkCapabilities networkCapabilities, + in Messenger messenger, int timeoutSec, in IBinder binder, int legacy); + + NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities, + in PendingIntent operation); + + NetworkRequest listenForNetwork(in NetworkCapabilities networkCapabilities, + in Messenger messenger, in IBinder binder); + + void pendingListenForNetwork(in NetworkCapabilities networkCapabilities, + in PendingIntent operation); + + void releaseNetworkRequest(in NetworkRequest networkRequest); + + int getRestoreDefaultNetworkDelay(int networkType); } diff --git a/core/java/android/net/IEthernetManager.aidl b/core/java/android/net/IEthernetManager.aidl new file mode 100644 index 0000000..3fa08f8 --- /dev/null +++ b/core/java/android/net/IEthernetManager.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.net; + +import android.net.IpConfiguration; + +/** + * Interface that answers queries about, and allows changing + * ethernet configuration. + */ +/** {@hide} */ +interface IEthernetManager +{ + IpConfiguration getConfiguration(); + void setConfiguration(in IpConfiguration config); +} diff --git a/core/java/android/net/IpConfiguration.aidl b/core/java/android/net/IpConfiguration.aidl new file mode 100644 index 0000000..7a30f0e --- /dev/null +++ b/core/java/android/net/IpConfiguration.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 IpConfiguration; diff --git a/core/java/android/net/IpConfiguration.java b/core/java/android/net/IpConfiguration.java new file mode 100644 index 0000000..4730bab --- /dev/null +++ b/core/java/android/net/IpConfiguration.java @@ -0,0 +1,151 @@ +/* + * 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.LinkProperties; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * A class representing a configured network. + * @hide + */ +public class IpConfiguration implements Parcelable { + private static final String TAG = "IpConfiguration"; + + public enum IpAssignment { + /* Use statically configured IP settings. Configuration can be accessed + * with linkProperties */ + STATIC, + /* Use dynamically configured IP settigns */ + DHCP, + /* no IP details are assigned, this is used to indicate + * that any existing IP settings should be retained */ + UNASSIGNED + } + + public IpAssignment ipAssignment; + + public enum ProxySettings { + /* No proxy is to be used. Any existing proxy settings + * should be cleared. */ + NONE, + /* Use statically configured proxy. Configuration can be accessed + * with linkProperties */ + STATIC, + /* no proxy details are assigned, this is used to indicate + * that any existing proxy settings should be retained */ + UNASSIGNED, + /* Use a Pac based proxy. + */ + PAC + } + + public ProxySettings proxySettings; + + public LinkProperties linkProperties; + + public IpConfiguration(IpConfiguration source) { + if (source != null) { + ipAssignment = source.ipAssignment; + proxySettings = source.proxySettings; + linkProperties = new LinkProperties(source.linkProperties); + } else { + ipAssignment = IpAssignment.UNASSIGNED; + proxySettings = ProxySettings.UNASSIGNED; + linkProperties = new LinkProperties(); + } + } + + public IpConfiguration() { + this(null); + } + + public IpConfiguration(IpAssignment ipAssignment, + ProxySettings proxySettings, + LinkProperties linkProperties) { + this.ipAssignment = ipAssignment; + this.proxySettings = proxySettings; + this.linkProperties = new LinkProperties(linkProperties); + } + + @Override + public String toString() { + StringBuilder sbuf = new StringBuilder(); + sbuf.append("IP assignment: " + ipAssignment.toString()); + sbuf.append("\n"); + sbuf.append("Proxy settings: " + proxySettings.toString()); + sbuf.append("\n"); + sbuf.append(linkProperties.toString()); + sbuf.append("\n"); + + return sbuf.toString(); + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + + if (!(o instanceof IpConfiguration)) { + return false; + } + + IpConfiguration other = (IpConfiguration) o; + return this.ipAssignment == other.ipAssignment && + this.proxySettings == other.proxySettings && + Objects.equals(this.linkProperties, other.linkProperties); + } + + @Override + public int hashCode() { + return 13 + (linkProperties != null ? linkProperties.hashCode() : 0) + + 17 * ipAssignment.ordinal() + + 47 * proxySettings.ordinal(); + } + + /** Implement the Parcelable interface */ + public int describeContents() { + return 0; + } + + /** Implement the Parcelable interface */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(ipAssignment.name()); + dest.writeString(proxySettings.name()); + dest.writeParcelable(linkProperties, flags); + } + + /** Implement the Parcelable interface */ + public static final Creator<IpConfiguration> CREATOR = + new Creator<IpConfiguration>() { + public IpConfiguration createFromParcel(Parcel in) { + IpConfiguration config = new IpConfiguration(); + config.ipAssignment = IpAssignment.valueOf(in.readString()); + config.proxySettings = ProxySettings.valueOf(in.readString()); + config.linkProperties = in.readParcelable(null); + return config; + } + + public IpConfiguration[] newArray(int size) { + return new IpConfiguration[size]; + } + }; +} diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java index a725bec..d07c0b61 100644 --- a/core/java/android/net/LinkAddress.java +++ b/core/java/android/net/LinkAddress.java @@ -39,7 +39,8 @@ import static android.system.OsConstants.RT_SCOPE_UNIVERSE; * <ul> * <li>An IP address and prefix length (e.g., {@code 2001:db8::1/64} or {@code 192.0.2.1/24}). * The address must be unicast, as multicast addresses cannot be assigned to interfaces. - * <li>Address flags: A bitmask of {@code IFA_F_*} values representing properties of the address. + * <li>Address flags: A bitmask of {@code IFA_F_*} values representing properties + * of the address. * <li>Address scope: An integer defining the scope in which the address is unique (e.g., * {@code RT_SCOPE_LINK} or {@code RT_SCOPE_SITE}). * <ul> @@ -47,10 +48,9 @@ import static android.system.OsConstants.RT_SCOPE_UNIVERSE; * When constructing a {@code LinkAddress}, the IP address and prefix are required. The flags and * scope are optional. If they are not specified, the flags are set to zero, and the scope will be * determined based on the IP address (e.g., link-local addresses will be created with a scope of - * {@code RT_SCOPE_LINK}, global addresses with {@code RT_SCOPE_UNIVERSE}, etc.) If they are - * specified, they are not checked for validity. + * {@code RT_SCOPE_LINK}, global addresses with {@code RT_SCOPE_UNIVERSE}, + * etc.) If they are specified, they are not checked for validity. * - * @hide */ public class LinkAddress implements Parcelable { /** @@ -119,6 +119,10 @@ public class LinkAddress implements Parcelable { * the specified flags and scope. Flags and scope are not checked for validity. * @param address The IP address. * @param prefixLength The prefix length. + * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address. + * @param scope An integer defining the scope in which the address is unique (e.g., + * {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}). + * @hide */ public LinkAddress(InetAddress address, int prefixLength, int flags, int scope) { init(address, prefixLength, flags, scope); @@ -129,6 +133,7 @@ public class LinkAddress implements Parcelable { * The flags are set to zero and the scope is determined from the address. * @param address The IP address. * @param prefixLength The prefix length. + * @hide */ public LinkAddress(InetAddress address, int prefixLength) { this(address, prefixLength, 0, 0); @@ -139,6 +144,7 @@ public class LinkAddress implements Parcelable { * Constructs a new {@code LinkAddress} from an {@code InterfaceAddress}. * The flags are set to zero and the scope is determined from the address. * @param interfaceAddress The interface address. + * @hide */ public LinkAddress(InterfaceAddress interfaceAddress) { this(interfaceAddress.getAddress(), @@ -149,6 +155,7 @@ public class LinkAddress implements Parcelable { * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or * "2001:db8::1/64". The flags are set to zero and the scope is determined from the address. * @param string The string to parse. + * @hide */ public LinkAddress(String address) { this(address, 0, 0); @@ -161,6 +168,7 @@ public class LinkAddress implements Parcelable { * @param string The string to parse. * @param flags The address flags. * @param scope The address scope. + * @hide */ public LinkAddress(String address, int flags, int scope) { InetAddress inetAddress = null; @@ -220,9 +228,10 @@ public class LinkAddress implements Parcelable { } /** - * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress} represent - * the same address. Two LinkAddresses represent the same address if they have the same IP - * address and prefix length, even if their properties are different. + * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress} + * represent the same address. Two {@code LinkAddresses} represent the same address + * if they have the same IP address and prefix length, even if their properties are + * different. * * @param other the {@code LinkAddress} to compare to. * @return {@code true} if both objects have the same address and prefix length, {@code false} @@ -233,28 +242,28 @@ public class LinkAddress implements Parcelable { } /** - * Returns the InetAddress of this address. + * Returns the {@link InetAddress} of this {@code LinkAddress}. */ public InetAddress getAddress() { return address; } /** - * Returns the prefix length of this address. + * Returns the prefix length of this {@code LinkAddress}. */ public int getNetworkPrefixLength() { return prefixLength; } /** - * Returns the flags of this address. + * Returns the flags of this {@code LinkAddress}. */ public int getFlags() { return flags; } /** - * Returns the scope of this address. + * Returns the scope of this {@code LinkAddress}. */ public int getScope() { return scope; @@ -262,6 +271,7 @@ public class LinkAddress implements Parcelable { /** * Returns true if this {@code LinkAddress} is global scope and preferred. + * @hide */ public boolean isGlobalPreferred() { return (scope == RT_SCOPE_UNIVERSE && diff --git a/core/java/android/net/LinkCapabilities.java b/core/java/android/net/LinkCapabilities.java deleted file mode 100644 index fb444ea..0000000 --- a/core/java/android/net/LinkCapabilities.java +++ /dev/null @@ -1,362 +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.net; - -import android.os.Parcelable; -import android.os.Parcel; -import android.util.Log; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -/** - * A class representing the capabilities of a link - * - * @hide - */ -public class LinkCapabilities implements Parcelable { - private static final String TAG = "LinkCapabilities"; - private static final boolean DBG = false; - - /** The Map of Keys to Values */ - private HashMap<Integer, String> mCapabilities; - - - /** - * The set of keys defined for a links capabilities. - * - * Keys starting with RW are read + write, i.e. the application - * can request for a certain requirement corresponding to that key. - * Keys starting with RO are read only, i.e. the the application - * can read the value of that key from the socket but cannot request - * a corresponding requirement. - * - * TODO: Provide a documentation technique for concisely and precisely - * define the syntax for each value string associated with a key. - */ - public static final class Key { - /** No constructor */ - private Key() {} - - /** - * An integer representing the network type. - * @see ConnectivityManager - */ - public final static int RO_NETWORK_TYPE = 1; - - /** - * Desired minimum forward link (download) bandwidth for the - * in kilobits per second (kbps). Values should be strings such - * "50", "100", "1500", etc. - */ - public final static int RW_DESIRED_FWD_BW = 2; - - /** - * Required minimum forward link (download) bandwidth, in - * per second (kbps), below which the socket cannot function. - * Values should be strings such as "50", "100", "1500", etc. - */ - public final static int RW_REQUIRED_FWD_BW = 3; - - /** - * Available forward link (download) bandwidth for the socket. - * This value is in kilobits per second (kbps). - * Values will be strings such as "50", "100", "1500", etc. - */ - public final static int RO_AVAILABLE_FWD_BW = 4; - - /** - * Desired minimum reverse link (upload) bandwidth for the socket - * in kilobits per second (kbps). - * Values should be strings such as "50", "100", "1500", etc. - * <p> - * This key is set via the needs map. - */ - public final static int RW_DESIRED_REV_BW = 5; - - /** - * Required minimum reverse link (upload) bandwidth, in kilobits - * per second (kbps), below which the socket cannot function. - * If a rate is not specified, the default rate of kbps will be - * Values should be strings such as "50", "100", "1500", etc. - */ - public final static int RW_REQUIRED_REV_BW = 6; - - /** - * Available reverse link (upload) bandwidth for the socket. - * This value is in kilobits per second (kbps). - * Values will be strings such as "50", "100", "1500", etc. - */ - public final static int RO_AVAILABLE_REV_BW = 7; - - /** - * Maximum latency for the socket, in milliseconds, above which - * socket cannot function. - * Values should be strings such as "50", "300", "500", etc. - */ - public final static int RW_MAX_ALLOWED_LATENCY = 8; - - /** - * Interface that the socket is bound to. This can be a virtual - * interface (e.g. VPN or Mobile IP) or a physical interface - * (e.g. wlan0 or rmnet0). - * Values will be strings such as "wlan0", "rmnet0" - */ - public final static int RO_BOUND_INTERFACE = 9; - - /** - * Physical interface that the socket is routed on. - * This can be different from BOUND_INTERFACE in cases such as - * VPN or Mobile IP. The physical interface may change over time - * if seamless mobility is supported. - * Values will be strings such as "wlan0", "rmnet0" - */ - public final static int RO_PHYSICAL_INTERFACE = 10; - } - - /** - * Role informs the LinkSocket about the data usage patterns of your - * application. - * <P> - * {@code Role.DEFAULT} is the default role, and is used whenever - * a role isn't set. - */ - public static final class Role { - /** No constructor */ - private Role() {} - - // examples only, discuss which roles should be defined, and then - // code these to match - - /** Default Role */ - public static final String DEFAULT = "default"; - /** Bulk down load */ - public static final String BULK_DOWNLOAD = "bulk.download"; - /** Bulk upload */ - public static final String BULK_UPLOAD = "bulk.upload"; - - /** VoIP Application at 24kbps */ - public static final String VOIP_24KBPS = "voip.24k"; - /** VoIP Application at 32kbps */ - public static final String VOIP_32KBPS = "voip.32k"; - - /** Video Streaming at 480p */ - public static final String VIDEO_STREAMING_480P = "video.streaming.480p"; - /** Video Streaming at 720p */ - public static final String VIDEO_STREAMING_720I = "video.streaming.720i"; - - /** Video Chat Application at 360p */ - public static final String VIDEO_CHAT_360P = "video.chat.360p"; - /** Video Chat Application at 480p */ - public static final String VIDEO_CHAT_480P = "video.chat.480i"; - } - - /** - * Constructor - */ - public LinkCapabilities() { - mCapabilities = new HashMap<Integer, String>(); - } - - /** - * Copy constructor. - * - * @param source - */ - public LinkCapabilities(LinkCapabilities source) { - if (source != null) { - mCapabilities = new HashMap<Integer, String>(source.mCapabilities); - } else { - mCapabilities = new HashMap<Integer, String>(); - } - } - - /** - * Create the {@code LinkCapabilities} with values depending on role type. - * @param applicationRole a {@code LinkSocket.Role} - * @return the {@code LinkCapabilities} associated with the applicationRole, empty if none - */ - public static LinkCapabilities createNeedsMap(String applicationRole) { - if (DBG) log("createNeededCapabilities(applicationRole) EX"); - return new LinkCapabilities(); - } - - /** - * Remove all capabilities - */ - public void clear() { - mCapabilities.clear(); - } - - /** - * Returns whether this map is empty. - */ - public boolean isEmpty() { - return mCapabilities.isEmpty(); - } - - /** - * Returns the number of elements in this map. - * - * @return the number of elements in this map. - */ - public int size() { - return mCapabilities.size(); - } - - /** - * Given the key return the capability string - * - * @param key - * @return the capability string - */ - public String get(int key) { - return mCapabilities.get(key); - } - - /** - * Store the key/value capability pair - * - * @param key - * @param value - */ - public void put(int key, String value) { - mCapabilities.put(key, value); - } - - /** - * Returns whether this map contains the specified key. - * - * @param key to search for. - * @return {@code true} if this map contains the specified key, - * {@code false} otherwise. - */ - public boolean containsKey(int key) { - return mCapabilities.containsKey(key); - } - - /** - * Returns whether this map contains the specified value. - * - * @param value to search for. - * @return {@code true} if this map contains the specified value, - * {@code false} otherwise. - */ - public boolean containsValue(String value) { - return mCapabilities.containsValue(value); - } - - /** - * Returns a set containing all of the mappings in this map. Each mapping is - * an instance of {@link Map.Entry}. As the set is backed by this map, - * changes in one will be reflected in the other. - * - * @return a set of the mappings. - */ - public Set<Entry<Integer, String>> entrySet() { - return mCapabilities.entrySet(); - } - - /** - * @return the set of the keys. - */ - public Set<Integer> keySet() { - return mCapabilities.keySet(); - } - - /** - * @return the set of values - */ - public Collection<String> values() { - return mCapabilities.values(); - } - - /** - * Implement the Parcelable interface - * @hide - */ - public int describeContents() { - return 0; - } - - /** - * Convert to string for debugging - */ - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("{"); - boolean firstTime = true; - for (Entry<Integer, String> entry : mCapabilities.entrySet()) { - if (firstTime) { - firstTime = false; - } else { - sb.append(","); - } - sb.append(entry.getKey()); - sb.append(":\""); - sb.append(entry.getValue()); - sb.append("\""); - } - sb.append("}"); - return sb.toString(); - } - - /** - * Implement the Parcelable interface. - * @hide - */ - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(mCapabilities.size()); - for (Entry<Integer, String> entry : mCapabilities.entrySet()) { - dest.writeInt(entry.getKey().intValue()); - dest.writeString(entry.getValue()); - } - } - - /** - * Implement the Parcelable interface. - * @hide - */ - public static final Creator<LinkCapabilities> CREATOR = - new Creator<LinkCapabilities>() { - public LinkCapabilities createFromParcel(Parcel in) { - LinkCapabilities capabilities = new LinkCapabilities(); - int size = in.readInt(); - while (size-- != 0) { - int key = in.readInt(); - String value = in.readString(); - capabilities.mCapabilities.put(key, value); - } - return capabilities; - } - - public LinkCapabilities[] newArray(int size) { - return new LinkCapabilities[size]; - } - }; - - /** - * Debug logging - */ - protected static void log(String s) { - Log.d(TAG, s); - } -} diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java index 2dcc544..3c36679 100644 --- a/core/java/android/net/LinkProperties.java +++ b/core/java/android/net/LinkProperties.java @@ -36,27 +36,12 @@ import java.util.Hashtable; * * A link represents a connection to a network. * It may have multiple addresses and multiple gateways, - * multiple dns servers but only one http proxy. + * multiple dns servers but only one http proxy and one + * network interface. * - * Because it's a single network, the dns's - * are interchangeable and don't need associating with - * particular addresses. The gateways similarly don't - * need associating with particular addresses. + * Note that this is just a holder of data. Modifying it + * does not affect live networks. * - * A dual stack interface works fine in this model: - * each address has it's own prefix length to describe - * the local network. The dns servers all return - * both v4 addresses and v6 addresses regardless of the - * address family of the server itself (rfc4213) and we - * don't care which is used. The gateways will be - * selected based on the destination address and the - * source address has no relavence. - * - * Links can also be stacked on top of each other. - * This can be used, for example, to represent a tunnel - * interface that runs on top of a physical interface. - * - * @hide */ public class LinkProperties implements Parcelable { // The interface described by the network link. @@ -73,6 +58,7 @@ public class LinkProperties implements Parcelable { private Hashtable<String, LinkProperties> mStackedLinks = new Hashtable<String, LinkProperties>(); + // @hide public static class CompareResult<T> { public Collection<T> removed = new ArrayList<T>(); public Collection<T> added = new ArrayList<T>(); @@ -89,10 +75,8 @@ public class LinkProperties implements Parcelable { } public LinkProperties() { - clear(); } - // copy constructor instead of clone public LinkProperties(LinkProperties source) { if (source != null) { mIfaceName = source.getInterfaceName(); @@ -109,6 +93,12 @@ public class LinkProperties implements Parcelable { } } + /** + * Sets the interface name for this link. All {@link RouteInfo} already set for this + * will have their interface changed to match this new value. + * + * @param iface The name of the network interface used for this link. + */ public void setInterfaceName(String iface) { mIfaceName = iface; ArrayList<RouteInfo> newRoutes = new ArrayList<RouteInfo>(mRoutes.size()); @@ -118,10 +108,16 @@ public class LinkProperties implements Parcelable { mRoutes = newRoutes; } + /** + * Gets the interface name for this link. May be {@code null} if not set. + * + * @return The interface name set for this link or {@code null}. + */ public String getInterfaceName() { return mIfaceName; } + // @hide public Collection<String> getAllInterfaceNames() { Collection interfaceNames = new ArrayList<String>(mStackedLinks.size() + 1); if (mIfaceName != null) interfaceNames.add(new String(mIfaceName)); @@ -132,7 +128,14 @@ public class LinkProperties implements Parcelable { } /** - * Returns all the addresses on this link. + * Returns all the addresses on this link. We often think of a link having a single address, + * however, particularly with Ipv6 several addresses are typical. Note that the + * {@code LinkProperties} actually contains {@link LinkAddress} objects which also include + * prefix lengths for each address. This is a simplified utility alternative to + * {@link LinkProperties#getLinkAddresses}. + * + * @return An umodifiable {@link Collection} of {@link InetAddress} for this link. + * @hide */ public Collection<InetAddress> getAddresses() { Collection<InetAddress> addresses = new ArrayList<InetAddress>(); @@ -144,6 +147,7 @@ public class LinkProperties implements Parcelable { /** * Returns all the addresses on this link and all the links stacked above it. + * @hide */ public Collection<InetAddress> getAllAddresses() { Collection<InetAddress> addresses = new ArrayList<InetAddress>(); @@ -166,7 +170,8 @@ public class LinkProperties implements Parcelable { } /** - * Adds a link address if it does not exist, or updates it if it does. + * Adds a {@link LinkAddress} to this {@code LinkProperties} if a {@link LinkAddress} of the + * same address/prefix does not already exist. If it does exist it is replaced. * @param address The {@code LinkAddress} to add. * @return true if {@code address} was added or updated, false otherwise. */ @@ -190,9 +195,10 @@ public class LinkProperties implements Parcelable { } /** - * Removes a link address. Specifically, removes the link address, if any, for which - * {@code isSameAddressAs(toRemove)} returns true. - * @param address A {@code LinkAddress} specifying the address to remove. + * Removes a {@link LinkAddress} from this {@code LinkProperties}. Specifically, matches + * and {@link LinkAddress} with the same address and prefix. + * + * @param toRemove A {@link LinkAddress} specifying the address to remove. * @return true if the address was removed, false if it did not exist. */ public boolean removeLinkAddress(LinkAddress toRemove) { @@ -205,7 +211,10 @@ public class LinkProperties implements Parcelable { } /** - * Returns all the addresses on this link. + * Returns all the {@link LinkAddress} on this link. Typically a link will have + * one IPv4 address and one or more IPv6 addresses. + * + * @return An unmodifiable {@link Collection} of {@link LinkAddress} for this link. */ public Collection<LinkAddress> getLinkAddresses() { return Collections.unmodifiableCollection(mLinkAddresses); @@ -213,6 +222,7 @@ public class LinkProperties implements Parcelable { /** * Returns all the addresses on this link and all the links stacked above it. + * @hide */ public Collection<LinkAddress> getAllLinkAddresses() { Collection<LinkAddress> addresses = new ArrayList<LinkAddress>(); @@ -224,7 +234,11 @@ public class LinkProperties implements Parcelable { } /** - * Replaces the LinkAddresses on this link with the given collection of addresses. + * Replaces the {@link LinkAddress} in this {@code LinkProperties} with + * the given {@link Collection} of {@link LinkAddress}. + * + * @param addresses The {@link Collection} of {@link LinkAddress} to set in this + * object. */ public void setLinkAddresses(Collection<LinkAddress> addresses) { mLinkAddresses.clear(); @@ -233,26 +247,64 @@ public class LinkProperties implements Parcelable { } } + /** + * Adds the given {@link InetAddress} to the list of DNS servers. + * + * @param dns The {@link InetAddress} to add to the list of DNS servers. + */ public void addDns(InetAddress dns) { if (dns != null) mDnses.add(dns); } + /** + * Returns all the {@link LinkAddress} for DNS servers on this link. + * + * @return An umodifiable {@link Collection} of {@link InetAddress} for DNS servers on + * this link. + */ public Collection<InetAddress> getDnses() { return Collections.unmodifiableCollection(mDnses); } - public String getDomains() { - return mDomains; - } - + /** + * Sets the DNS domain search path used on this link. + * + * @param domains A {@link String} listing in priority order the comma separated + * domains to search when resolving host names on this link. + */ public void setDomains(String domains) { mDomains = domains; } + /** + * Get the DNS domains search path set for this link. + * + * @return A {@link String} containing the comma separated domains to search when resolving + * host names on this link. + */ + public String getDomains() { + return mDomains; + } + + /** + * Sets the Maximum Transmission Unit size to use on this link. This should not be used + * unless the system default (1500) is incorrect. Values less than 68 or greater than + * 10000 will be ignored. + * + * @param mtu The MTU to use for this link. + * @hide + */ public void setMtu(int mtu) { mMtu = mtu; } + /** + * Gets any non-default MTU size set for this link. Note that if the default is being used + * this will return 0. + * + * @return The mtu value set for this link. + * @hide + */ public int getMtu() { return mMtu; } @@ -264,6 +316,14 @@ public class LinkProperties implements Parcelable { mIfaceName); } + /** + * Adds a {@link RouteInfo} to this {@code LinkProperties}. If the {@link RouteInfo} + * had an interface name set and that differs from the interface set for this + * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown. The + * proper course is to add either un-named or properly named {@link RouteInfo}. + * + * @param route A {@link RouteInfo} to add to this object. + */ public void addRoute(RouteInfo route) { if (route != null) { String routeIface = route.getInterface(); @@ -277,7 +337,9 @@ public class LinkProperties implements Parcelable { } /** - * Returns all the routes on this link. + * Returns all the {@link RouteInfo} set on this link. + * + * @return An unmodifiable {@link Collection} of {@link RouteInfo} for this link. */ public Collection<RouteInfo> getRoutes() { return Collections.unmodifiableCollection(mRoutes); @@ -285,6 +347,7 @@ public class LinkProperties implements Parcelable { /** * Returns all the routes on this link and all the links stacked above it. + * @hide */ public Collection<RouteInfo> getAllRoutes() { Collection<RouteInfo> routes = new ArrayList(); @@ -295,9 +358,22 @@ public class LinkProperties implements Parcelable { return routes; } + /** + * Sets the recommended {@link ProxyInfo} to use on this link, or {@code null} for none. + * Note that Http Proxies are only a hint - the system recommends their use, but it does + * not enforce it and applications may ignore them. + * + * @param proxy A {@link ProxyInfo} defining the Http Proxy to use on this link. + */ public void setHttpProxy(ProxyInfo proxy) { mHttpProxy = proxy; } + + /** + * Gets the recommended {@link ProxyInfo} (or {@code null}) set on this link. + * + * @return The {@link ProxyInfo} set on this link + */ public ProxyInfo getHttpProxy() { return mHttpProxy; } @@ -311,6 +387,7 @@ public class LinkProperties implements Parcelable { * * @param link The link to add. * @return true if the link was stacked, false otherwise. + * @hide */ public boolean addStackedLink(LinkProperties link) { if (link != null && link.getInterfaceName() != null) { @@ -328,6 +405,7 @@ public class LinkProperties implements Parcelable { * * @param link The link to remove. * @return true if the link was removed, false otherwise. + * @hide */ public boolean removeStackedLink(LinkProperties link) { if (link != null && link.getInterfaceName() != null) { @@ -339,6 +417,7 @@ public class LinkProperties implements Parcelable { /** * Returns all the links stacked on top of this link. + * @hide */ public Collection<LinkProperties> getStackedLinks() { Collection<LinkProperties> stacked = new ArrayList<LinkProperties>(); @@ -348,6 +427,9 @@ public class LinkProperties implements Parcelable { return Collections.unmodifiableCollection(stacked); } + /** + * Clears this object to its initial state. + */ public void clear() { mIfaceName = null; mLinkAddresses.clear(); @@ -433,6 +515,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalInterfaceName(LinkProperties target) { return TextUtils.equals(getInterfaceName(), target.getInterfaceName()); @@ -443,6 +526,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalAddresses(LinkProperties target) { Collection<InetAddress> targetAddresses = target.getAddresses(); @@ -456,6 +540,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalDnses(LinkProperties target) { Collection<InetAddress> targetDnses = target.getDnses(); @@ -474,6 +559,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalRoutes(LinkProperties target) { Collection<RouteInfo> targetRoutes = target.getRoutes(); @@ -486,6 +572,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalHttpProxy(LinkProperties target) { return getHttpProxy() == null ? target.getHttpProxy() == null : @@ -497,6 +584,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalStackedLinks(LinkProperties target) { if (!mStackedLinks.keySet().equals(target.mStackedLinks.keySet())) { @@ -517,6 +605,7 @@ public class LinkProperties implements Parcelable { * * @param target LinkProperties to compare. * @return {@code true} if both are identical, {@code false} otherwise. + * @hide */ public boolean isIdenticalMtu(LinkProperties target) { return getMtu() == target.getMtu(); @@ -534,10 +623,6 @@ public class LinkProperties implements Parcelable { * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal. * 2. Worst case performance is O(n^2). * - * This method does not check that stacked interfaces are equal, because - * stacked interfaces are not so much a property of the link as a - * description of connections between links. - * * @param obj the object to be tested for equality. * @return {@code true} if both objects are equal, {@code false} otherwise. */ @@ -547,7 +632,11 @@ public class LinkProperties implements Parcelable { if (!(obj instanceof LinkProperties)) return false; LinkProperties target = (LinkProperties) obj; - + /** + * This method does not check that stacked interfaces are equal, because + * stacked interfaces are not so much a property of the link as a + * description of connections between links. + */ return isIdenticalInterfaceName(target) && isIdenticalAddresses(target) && isIdenticalDnses(target) && @@ -563,6 +652,7 @@ public class LinkProperties implements Parcelable { * * @param target a LinkProperties with the new list of addresses * @return the differences between the addresses. + * @hide */ public CompareResult<LinkAddress> compareAddresses(LinkProperties target) { /* @@ -591,6 +681,7 @@ public class LinkProperties implements Parcelable { * * @param target a LinkProperties with the new list of dns addresses * @return the differences between the DNS addresses. + * @hide */ public CompareResult<InetAddress> compareDnses(LinkProperties target) { /* @@ -620,6 +711,7 @@ public class LinkProperties implements Parcelable { * * @param target a LinkProperties with the new list of routes * @return the differences between the routes. + * @hide */ public CompareResult<RouteInfo> compareAllRoutes(LinkProperties target) { /* @@ -642,6 +734,35 @@ public class LinkProperties implements Parcelable { return result; } + /** + * Compares all interface names in this LinkProperties with another + * LinkProperties, examining both the the base link and all stacked links. + * + * @param target a LinkProperties with the new list of interface names + * @return the differences between the interface names. + * @hide + */ + public CompareResult<String> compareAllInterfaceNames(LinkProperties target) { + /* + * Duplicate the interface names into removed, we will be removing + * interface names which are common between this and target + * leaving the interface names that are different. And interface names which + * are in target but not in this are placed in added. + */ + CompareResult<String> result = new CompareResult<String>(); + + result.removed = getAllInterfaceNames(); + result.added.clear(); + if (target != null) { + for (String r : target.getAllInterfaceNames()) { + if (! result.removed.remove(r)) { + result.added.add(r); + } + } + } + return result; + } + @Override /** diff --git a/core/java/android/net/LinkSocket.java b/core/java/android/net/LinkSocket.java deleted file mode 100644 index 5aa6451..0000000 --- a/core/java/android/net/LinkSocket.java +++ /dev/null @@ -1,276 +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.net; - -import android.net.LinkCapabilities; -import android.net.LinkProperties; -import android.net.LinkSocketNotifier; - -import android.util.Log; - -import java.io.IOException; -import java.net.Socket; -import java.net.SocketAddress; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; -import java.util.HashSet; -import java.util.Set; - -/** @hide */ -public class LinkSocket extends Socket { - private final static String TAG = "LinkSocket"; - private final static boolean DBG = true; - - /** - * Default constructor - */ - public LinkSocket() { - if (DBG) log("LinkSocket() EX"); - } - - /** - * Creates a new unconnected socket. - * @param notifier a reference to a class that implements {@code LinkSocketNotifier} - */ - public LinkSocket(LinkSocketNotifier notifier) { - if (DBG) log("LinkSocket(notifier) EX"); - } - - /** - * Creates a new unconnected socket usign the given proxy type. - * @param notifier a reference to a class that implements {@code LinkSocketNotifier} - * @param proxy the specified proxy for this socket - * @throws IllegalArgumentException if the argument proxy is null or of an invalid type. - * @throws SecurityException if a security manager exists and it denies the permission - * to connect to the given proxy. - */ - public LinkSocket(LinkSocketNotifier notifier, Proxy proxy) { - if (DBG) log("LinkSocket(notifier, proxy) EX"); - } - - /** - * @return the {@code LinkProperties} for the socket - */ - public LinkProperties getLinkProperties() { - if (DBG) log("LinkProperties() EX"); - return new LinkProperties(); - } - - /** - * Set the {@code LinkCapabilies} needed for this socket. If the socket is already connected - * or is a duplicate socket the request is ignored and {@code false} will - * be returned. A needs map can be created via the {@code createNeedsMap} static - * method. - * @param needs the needs of the socket - * @return {@code true} if needs are successfully set, {@code false} otherwise - */ - public boolean setNeededCapabilities(LinkCapabilities needs) { - if (DBG) log("setNeeds() EX"); - return false; - } - - /** - * @return the LinkCapabilites set by setNeededCapabilities, empty if none has been set - */ - public LinkCapabilities getNeededCapabilities() { - if (DBG) log("getNeeds() EX"); - return null; - } - - /** - * @return all of the {@code LinkCapabilities} of the link used by this socket - */ - public LinkCapabilities getCapabilities() { - if (DBG) log("getCapabilities() EX"); - return null; - } - - /** - * Returns this LinkSockets set of capabilities, filtered according to - * the given {@code Set}. Capabilities in the Set but not available from - * the link will not be reported in the results. Capabilities of the link - * but not listed in the Set will also not be reported in the results. - * @param capabilities {@code Set} of capabilities requested - * @return the filtered {@code LinkCapabilities} of this LinkSocket, may be empty - */ - public LinkCapabilities getCapabilities(Set<Integer> capabilities) { - if (DBG) log("getCapabilities(capabilities) EX"); - return new LinkCapabilities(); - } - - /** - * Provide the set of capabilities the application is interested in tracking - * for this LinkSocket. - * @param capabilities a {@code Set} of capabilities to track - */ - public void setTrackedCapabilities(Set<Integer> capabilities) { - if (DBG) log("setTrackedCapabilities(capabilities) EX"); - } - - /** - * @return the {@code LinkCapabilities} that are tracked, empty if none has been set. - */ - public Set<Integer> getTrackedCapabilities() { - if (DBG) log("getTrackedCapabilities(capabilities) EX"); - return new HashSet<Integer>(); - } - - /** - * Connects this socket to the given remote host address and port specified - * by dstName and dstPort. - * @param dstName the address of the remote host to connect to - * @param dstPort the port to connect to on the remote host - * @param timeout the timeout value in milliseconds or 0 for infinite timeout - * @throws UnknownHostException if the given dstName is invalid - * @throws IOException if the socket is already connected or an error occurs - * while connecting - * @throws SocketTimeoutException if the timeout fires - */ - public void connect(String dstName, int dstPort, int timeout) - throws UnknownHostException, IOException, SocketTimeoutException { - if (DBG) log("connect(dstName, dstPort, timeout) EX"); - } - - /** - * Connects this socket to the given remote host address and port specified - * by dstName and dstPort. - * @param dstName the address of the remote host to connect to - * @param dstPort the port to connect to on the remote host - * @throws UnknownHostException if the given dstName is invalid - * @throws IOException if the socket is already connected or an error occurs - * while connecting - */ - public void connect(String dstName, int dstPort) - throws UnknownHostException, IOException { - if (DBG) log("connect(dstName, dstPort, timeout) EX"); - } - - /** - * Connects this socket to the given remote host address and port specified - * by the SocketAddress with the specified timeout. - * @deprecated Use {@code connect(String dstName, int dstPort, int timeout)} - * instead. Using this method may result in reduced functionality. - * @param remoteAddr the address and port of the remote host to connect to - * @throws IllegalArgumentException if the given SocketAddress is invalid - * @throws IOException if the socket is already connected or an error occurs - * while connecting - * @throws SocketTimeoutException if the timeout expires - */ - @Override - @Deprecated - public void connect(SocketAddress remoteAddr, int timeout) - throws IOException, SocketTimeoutException { - if (DBG) log("connect(remoteAddr, timeout) EX DEPRECATED"); - } - - /** - * Connects this socket to the given remote host address and port specified - * by the SocketAddress. - * TODO add comment on all these that the network selection happens during connect - * and may take 30 seconds - * @deprecated Use {@code connect(String dstName, int dstPort)} - * Using this method may result in reduced functionality. - * @param remoteAddr the address and port of the remote host to connect to. - * @throws IllegalArgumentException if the SocketAddress is invalid or not supported. - * @throws IOException if the socket is already connected or an error occurs - * while connecting - */ - @Override - @Deprecated - public void connect(SocketAddress remoteAddr) throws IOException { - if (DBG) log("connect(remoteAddr) EX DEPRECATED"); - } - - /** - * Connect a duplicate socket socket to the same remote host address and port - * as the original with a timeout parameter. - * @param timeout the timeout value in milliseconds or 0 for infinite timeout - * @throws IOException if the socket is already connected or an error occurs - * while connecting - */ - public void connect(int timeout) throws IOException { - if (DBG) log("connect(timeout) EX"); - } - - /** - * Connect a duplicate socket socket to the same remote host address and port - * as the original. - * @throws IOException if the socket is already connected or an error occurs - * while connecting - */ - public void connect() throws IOException { - if (DBG) log("connect() EX"); - } - - /** - * Closes the socket. It is not possible to reconnect or rebind to this - * socket thereafter which means a new socket instance has to be created. - * @throws IOException if an error occurs while closing the socket - */ - @Override - public synchronized void close() throws IOException { - if (DBG) log("close() EX"); - } - - /** - * Request that a new LinkSocket be created using a different radio - * (such as WiFi or 3G) than the current LinkSocket. If a different - * radio is available a call back will be made via {@code onBetterLinkAvail}. - * If unable to find a better radio, application will be notified via - * {@code onNewLinkUnavailable} - * @see LinkSocketNotifier#onBetterLinkAvailable(LinkSocket, LinkSocket) - * @param linkRequestReason reason for requesting a new link. - */ - public void requestNewLink(LinkRequestReason linkRequestReason) { - if (DBG) log("requestNewLink(linkRequestReason) EX"); - } - - /** - * @deprecated LinkSocket will automatically pick the optimum interface - * to bind to - * @param localAddr the specific address and port on the local machine - * to bind to - * @throws IOException always as this method is deprecated for LinkSocket - */ - @Override - @Deprecated - public void bind(SocketAddress localAddr) throws UnsupportedOperationException { - if (DBG) log("bind(localAddr) EX throws IOException"); - throw new UnsupportedOperationException("bind is deprecated for LinkSocket"); - } - - /** - * Reason codes an application can specify when requesting for a new link. - * TODO: need better documentation - */ - public static final class LinkRequestReason { - /** No constructor */ - private LinkRequestReason() {} - - /** This link is working properly */ - public static final int LINK_PROBLEM_NONE = 0; - /** This link has an unknown issue */ - public static final int LINK_PROBLEM_UNKNOWN = 1; - } - - /** - * Debug logging - */ - protected static void log(String s) { - Log.d(TAG, s); - } -} diff --git a/core/java/android/net/LinkSocketNotifier.java b/core/java/android/net/LinkSocketNotifier.java deleted file mode 100644 index e2429d8..0000000 --- a/core/java/android/net/LinkSocketNotifier.java +++ /dev/null @@ -1,86 +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.net; - -/** - * 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. - * @hide - */ -public interface LinkSocketNotifier { - /** - * This callback function will be called if a better link - * becomes available. - * TODO - this shouldn't be checked for all cases - what's the conditional - * flag? - * If the duplicate socket is accepted, the original will be marked invalid - * and additional use will throw exceptions. - * @param original the original LinkSocket - * @param duplicate the new LinkSocket that better meets the application - * requirements - * @return {@code true} if the application intends to use this link - * - * REM - * TODO - how agressive should we be? - * At a minimum CS tracks which LS have this turned on and tracks the requirements - * When a new link becomes available, automatically check if any of the LinkSockets - * will care. - * If found, grab a refcount on the link so it doesn't go away and send notification - * Optionally, periodically setup connection on available networks to check for better links - * Maybe pass this info into the LinkFactories so condition changes can be acted on more quickly - */ - public boolean onBetterLinkAvailable(LinkSocket original, LinkSocket duplicate); - - /** - * This callback function will be called when a LinkSocket no longer has - * an active link. - * @param socket the LinkSocket that lost its link - * - * REM - * NetworkStateTracker tells us it is disconnected - * CS checks the table for LS on that link - * CS calls each callback (need to work out p2p cross process callback) - */ - public void onLinkLost(LinkSocket socket); - - /** - * This callback function will be called when an application calls - * requestNewLink on a LinkSocket but the LinkSocket is unable to find - * a suitable new link. - * @param socket the LinkSocket for which a new link was not found - * TODO - why the diff between initial request (sync) and requestNewLink? - * - * REM - * CS process of trying to find a new link must track the LS that started it - * on failure, call callback - */ - public void onNewLinkUnavailable(LinkSocket socket); - - /** - * This callback function will be called when any of the notification-marked - * capabilities of the LinkSocket (e.g. upstream bandwidth) have changed. - * @param socket the linkSocet for which capabilities have changed - * @param changedCapabilities the set of capabilities that the application - * is interested in and have changed (with new values) - * - * REM - * Maybe pass the interesting capabilities into the Links. - * Get notified of every capability change - * check for LinkSockets on that Link that are interested in that Capability - call them - */ - public void onCapabilitiesChanged(LinkSocket socket, LinkCapabilities changedCapabilities); -} diff --git a/core/java/android/net/LocalSocketImpl.java b/core/java/android/net/LocalSocketImpl.java index 643e8c2..fa9f479 100644 --- a/core/java/android/net/LocalSocketImpl.java +++ b/core/java/android/net/LocalSocketImpl.java @@ -56,7 +56,10 @@ class LocalSocketImpl /** {@inheritDoc} */ @Override public int available() throws IOException { - return available_native(fd); + FileDescriptor myFd = fd; + if (myFd == null) throw new IOException("socket closed"); + + return available_native(myFd); } /** {@inheritDoc} */ diff --git a/core/java/android/net/MobileDataStateTracker.java b/core/java/android/net/MobileDataStateTracker.java index 30b61c5..535bbe2 100644 --- a/core/java/android/net/MobileDataStateTracker.java +++ b/core/java/android/net/MobileDataStateTracker.java @@ -66,7 +66,6 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { private Handler mTarget; private Context mContext; private LinkProperties mLinkProperties; - private LinkCapabilities mLinkCapabilities; private boolean mPrivateDnsRouteSet = false; private boolean mDefaultRouteSet = false; @@ -200,11 +199,11 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { } mLinkProperties.setMtu(mContext.getResources().getInteger( com.android.internal.R.integer.config_mobile_mtu)); - mLinkCapabilities = intent.getParcelableExtra( - PhoneConstants.DATA_LINK_CAPABILITIES_KEY); - if (mLinkCapabilities == null) { - loge("CONNECTED event did not supply link capabilities."); - mLinkCapabilities = new LinkCapabilities(); + mNetworkCapabilities = intent.getParcelableExtra( + PhoneConstants.DATA_NETWORK_CAPABILITIES_KEY); + if (mNetworkCapabilities == null) { + loge("CONNECTED event did not supply network capabilities."); + mNetworkCapabilities = new NetworkCapabilities(); } } @@ -316,10 +315,10 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { Slog.d(TAG, "LinkProperties = " ); } - if (mLinkCapabilities != null) { - Slog.d(TAG, "LinkCapabilities = " + mLinkCapabilities); + if (mNetworkCapabilities != null) { + Slog.d(TAG, mNetworkCapabilities.toString()); } else { - Slog.d(TAG, "LinkCapabilities = " ); + Slog.d(TAG, "NetworkCapabilities = " ); } } @@ -750,14 +749,6 @@ public class MobileDataStateTracker extends BaseNetworkStateTracker { return new LinkProperties(mLinkProperties); } - /** - * @see android.net.NetworkStateTracker#getLinkCapabilities() - */ - @Override - public LinkCapabilities getLinkCapabilities() { - return new LinkCapabilities(mLinkCapabilities); - } - public void supplyMessenger(Messenger messenger) { if (VDBG) log(mApnType + " got supplyMessenger"); AsyncChannel ac = new AsyncChannel(); diff --git a/core/java/android/net/LinkCapabilities.aidl b/core/java/android/net/Network.aidl index df72599..73ba1af 100644 --- a/core/java/android/net/LinkCapabilities.aidl +++ b/core/java/android/net/Network.aidl @@ -1,6 +1,6 @@ /* ** -** Copyright (C) 2010 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. @@ -17,5 +17,4 @@ package android.net; -parcelable LinkCapabilities; - +parcelable Network; diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java new file mode 100644 index 0000000..64516e6 --- /dev/null +++ b/core/java/android/net/Network.java @@ -0,0 +1,250 @@ +/* + * 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.NetworkUtils; +import android.os.Parcelable; +import android.os.Parcel; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import javax.net.SocketFactory; + +/** + * Identifies a {@code Network}. This is supplied to applications via + * {@link ConnectivityManager.NetworkCallbackListener} in response to + * {@link ConnectivityManager#requestNetwork} or {@link ConnectivityManager#listenForNetwork}. + * It is used to direct traffic to the given {@code Network}, either on a {@link Socket} basis + * through a targeted {@link SocketFactory} or process-wide via {@link #bindProcess}. + */ +public class Network implements Parcelable { + + /** + * @hide + */ + public final int netId; + + private NetworkBoundSocketFactory mNetworkBoundSocketFactory = null; + + /** + * @hide + */ + public Network(int netId) { + this.netId = netId; + } + + /** + * @hide + */ + public Network(Network that) { + this.netId = that.netId; + } + + /** + * Operates the same as {@code InetAddress.getAllByName} except that host + * resolution is done on this network. + * + * @param host the hostname or literal IP string to be resolved. + * @return the array of addresses associated with the specified host. + * @throws UnknownHostException if the address lookup fails. + */ + public InetAddress[] getAllByName(String host) throws UnknownHostException { + return InetAddress.getAllByNameOnNet(host, netId); + } + + /** + * Operates the same as {@code InetAddress.getByName} except that host + * resolution is done on this network. + * + * @param host + * the hostName to be resolved to an address or {@code null}. + * @return the {@code InetAddress} instance representing the host. + * @throws UnknownHostException + * if the address lookup fails. + */ + public InetAddress getByName(String host) throws UnknownHostException { + return InetAddress.getByNameOnNet(host, netId); + } + + /** + * A {@code SocketFactory} that produces {@code Socket}'s bound to this network. + */ + private class NetworkBoundSocketFactory extends SocketFactory { + private final int mNetId; + + public NetworkBoundSocketFactory(int netId) { + super(); + mNetId = netId; + } + + private void connectToHost(Socket socket, String host, int port) throws IOException { + // Lookup addresses only on this Network. + InetAddress[] hostAddresses = getAllByName(host); + // Try all but last address ignoring exceptions. + for (int i = 0; i < hostAddresses.length - 1; i++) { + try { + socket.connect(new InetSocketAddress(hostAddresses[i], port)); + return; + } catch (IOException e) { + } + } + // Try last address. Do throw exceptions. + socket.connect(new InetSocketAddress(hostAddresses[hostAddresses.length - 1], port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { + Socket socket = createSocket(); + socket.bind(new InetSocketAddress(localHost, localPort)); + connectToHost(socket, host, port); + return socket; + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, + int localPort) throws IOException { + Socket socket = createSocket(); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(address, port)); + return socket; + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + Socket socket = createSocket(); + socket.connect(new InetSocketAddress(host, port)); + return socket; + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + Socket socket = createSocket(); + connectToHost(socket, host, port); + return socket; + } + + @Override + public Socket createSocket() throws IOException { + Socket socket = new Socket(); + // Query a property of the underlying socket to ensure the underlying + // socket exists so a file descriptor is available to bind to a network. + socket.getReuseAddress(); + NetworkUtils.bindSocketToNetwork(socket.getFileDescriptor$().getInt$(), mNetId); + return socket; + } + } + + /** + * Returns a {@link SocketFactory} bound to this network. Any {@link Socket} created by + * this factory will have its traffic sent over this {@code Network}. Note that if this + * {@code Network} ever disconnects, this factory and any {@link Socket} it produced in the + * past or future will cease to work. + * + * @return a {@link SocketFactory} which produces {@link Socket} instances bound to this + * {@code Network}. + */ + public SocketFactory socketFactory() { + if (mNetworkBoundSocketFactory == null) { + mNetworkBoundSocketFactory = new NetworkBoundSocketFactory(netId); + } + return mNetworkBoundSocketFactory; + } + + /** + * Binds the current process to this network. All sockets created in the future (and not + * explicitly bound via a bound {@link SocketFactory} (see {@link Network#socketFactory}) + * will be bound to this network. Note that if this {@code Network} ever disconnects + * all sockets created in this way will cease to work. This is by design so an application + * doesn't accidentally use sockets it thinks are still bound to a particular {@code Network}. + */ + public void bindProcess() { + NetworkUtils.bindProcessToNetwork(netId); + } + + /** + * Binds host resolutions performed by this process to this network. {@link #bindProcess} + * takes precedence over this setting. + * + * @hide + * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature(). + */ + public void bindProcessForHostResolution() { + NetworkUtils.bindProcessToNetworkForHostResolution(netId); + } + + /** + * Clears any process specific {@link Network} binding for host resolution. This does + * not clear bindings enacted via {@link #bindProcess}. + * + * @hide + * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature(). + */ + public void unbindProcessForHostResolution() { + NetworkUtils.unbindProcessToNetworkForHostResolution(); + } + + /** + * A static utility method to return any {@code Network} currently bound by this process. + * + * @return {@code Network} to which this process is bound. + */ + public static Network getProcessBoundNetwork() { + return new Network(NetworkUtils.getNetworkBoundToProcess()); + } + + /** + * Clear any process specific {@code Network} binding. This reverts a call to + * {@link Network#bindProcess}. + */ + public static void unbindProcess() { + NetworkUtils.unbindProcessToNetwork(); + } + + // implement the Parcelable interface + public int describeContents() { + return 0; + } + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(netId); + } + + public static final Creator<Network> CREATOR = + new Creator<Network>() { + public Network createFromParcel(Parcel in) { + int netId = in.readInt(); + + return new Network(netId); + } + + public Network[] newArray(int size) { + return new Network[size]; + } + }; + + public boolean equals(Object obj) { + if (obj instanceof Network == false) return false; + Network other = (Network)obj; + return this.netId == other.netId; + } + + public int hashCode() { + return netId * 11; + } +} diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java new file mode 100644 index 0000000..7e8b1f1 --- /dev/null +++ b/core/java/android/net/NetworkAgent.java @@ -0,0 +1,202 @@ +/* + * 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.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +import com.android.internal.util.AsyncChannel; +import com.android.internal.util.Protocol; + +import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * A Utility class for handling for communicating between bearer-specific + * code and ConnectivityService. + * + * A bearer may have more than one NetworkAgent if it can simultaneously + * support separate networks (IMS / Internet / MMS Apns on cellular, or + * perhaps connections with different SSID or P2P for Wi-Fi). + * + * @hide + */ +public abstract class NetworkAgent extends Handler { + private volatile AsyncChannel mAsyncChannel; + private final String LOG_TAG; + private static final boolean DBG = true; + private static final boolean VDBG = true; + private final Context mContext; + private final ArrayList<Message>mPreConnectedQueue = new ArrayList<Message>(); + + private static final int BASE = Protocol.BASE_NETWORK_AGENT; + + /** + * Sent by ConnectivityService to the NetworkAgent to inform it of + * suspected connectivity problems on its network. The NetworkAgent + * should take steps to verify and correct connectivity. + */ + public static final int CMD_SUSPECT_BAD = BASE; + + /** + * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to + * ConnectivityService to pass the current NetworkInfo (connection state). + * Sent when the NetworkInfo changes, mainly due to change of state. + * obj = NetworkInfo + */ + public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 1; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * NetworkCapabilties. + * obj = NetworkCapabilities + */ + public static final int EVENT_NETWORK_CAPABILITIES_CHANGED = BASE + 2; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * NetworkProperties. + * obj = NetworkProperties + */ + public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3; + + /** + * Sent by the NetworkAgent to ConnectivityService to pass the current + * network score. + * obj = network score Integer + */ + public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4; + + public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni, + NetworkCapabilities nc, LinkProperties lp, int score) { + super(looper); + LOG_TAG = logTag; + mContext = context; + if (ni == null || nc == null || lp == null) { + throw new IllegalArgumentException(); + } + + if (DBG) log("Registering NetworkAgent"); + ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni), + new LinkProperties(lp), new NetworkCapabilities(nc), score); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: { + if (mAsyncChannel != null) { + log("Received new connection while already connected!"); + } else { + if (DBG) log("NetworkAgent fully connected"); + AsyncChannel ac = new AsyncChannel(); + ac.connected(null, this, msg.replyTo); + ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, + AsyncChannel.STATUS_SUCCESSFUL); + synchronized (mPreConnectedQueue) { + mAsyncChannel = ac; + for (Message m : mPreConnectedQueue) { + ac.sendMessage(m); + } + mPreConnectedQueue.clear(); + } + } + break; + } + case AsyncChannel.CMD_CHANNEL_DISCONNECT: { + if (DBG) log("CMD_CHANNEL_DISCONNECT"); + if (mAsyncChannel != null) mAsyncChannel.disconnect(); + break; + } + case AsyncChannel.CMD_CHANNEL_DISCONNECTED: { + if (DBG) log("NetworkAgent channel lost"); + // let the client know CS is done with us. + unwanted(); + synchronized (mPreConnectedQueue) { + mAsyncChannel = null; + } + break; + } + case CMD_SUSPECT_BAD: { + log("Unhandled Message " + msg); + break; + } + } + } + + private void queueOrSendMessage(int what, Object obj) { + synchronized (mPreConnectedQueue) { + if (mAsyncChannel != null) { + mAsyncChannel.sendMessage(what, obj); + } else { + Message msg = Message.obtain(); + msg.what = what; + msg.obj = obj; + mPreConnectedQueue.add(msg); + } + } + } + + /** + * Called by the bearer code when it has new LinkProperties data. + */ + public void sendLinkProperties(LinkProperties linkProperties) { + queueOrSendMessage(EVENT_NETWORK_PROPERTIES_CHANGED, new LinkProperties(linkProperties)); + } + + /** + * Called by the bearer code when it has new NetworkInfo data. + */ + public void sendNetworkInfo(NetworkInfo networkInfo) { + queueOrSendMessage(EVENT_NETWORK_INFO_CHANGED, new NetworkInfo(networkInfo)); + } + + /** + * Called by the bearer code when it has new NetworkCapabilities data. + */ + public void sendNetworkCapabilities(NetworkCapabilities networkCapabilities) { + queueOrSendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED, + new NetworkCapabilities(networkCapabilities)); + } + + /** + * Called by the bearer code when it has a new score for this network. + */ + public void sendNetworkScore(int score) { + queueOrSendMessage(EVENT_NETWORK_SCORE_CHANGED, new Integer(score)); + } + + /** + * Called when ConnectivityService has indicated they no longer want this network. + * The parent factory should (previously) have received indication of the change + * as well, either canceling NetworkRequests or altering their score such that this + * network won't be immediately requested again. + */ + abstract protected void unwanted(); + + protected void log(String s) { + Log.d(LOG_TAG, "NetworkAgent: " + s); + } +} diff --git a/core/java/android/net/NetworkCapabilities.aidl b/core/java/android/net/NetworkCapabilities.aidl new file mode 100644 index 0000000..cd7d71c --- /dev/null +++ b/core/java/android/net/NetworkCapabilities.aidl @@ -0,0 +1,21 @@ +/* +** +** 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 NetworkCapabilities; + diff --git a/core/java/android/net/NetworkCapabilities.java b/core/java/android/net/NetworkCapabilities.java new file mode 100644 index 0000000..35274f1 --- /dev/null +++ b/core/java/android/net/NetworkCapabilities.java @@ -0,0 +1,510 @@ +/* + * 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 android.util.Log; + +import java.lang.IllegalArgumentException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * This class represents the capabilities of a network. This is used both to specify + * needs to {@link ConnectivityManager} and when inspecting a network. + * + * Note that this replaces the old {@link ConnectivityManager#TYPE_MOBILE} method + * of network selection. Rather than indicate a need for Wi-Fi because an application + * needs high bandwidth and risk obselence when a new, fast network appears (like LTE), + * the application should specify it needs high bandwidth. Similarly if an application + * needs an unmetered network for a bulk transfer it can specify that rather than assuming + * all cellular based connections are metered and all Wi-Fi based connections are not. + */ +public final class NetworkCapabilities implements Parcelable { + private static final String TAG = "NetworkCapabilities"; + private static final boolean DBG = false; + + public NetworkCapabilities() { + } + + public NetworkCapabilities(NetworkCapabilities nc) { + if (nc != null) { + mNetworkCapabilities = nc.mNetworkCapabilities; + mTransportTypes = nc.mTransportTypes; + mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps; + mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps; + } + } + + /** + * Represents the network's capabilities. If any are specified they will be satisfied + * by any Network that matches all of them. + */ + private long mNetworkCapabilities = (1 << NET_CAPABILITY_NOT_RESTRICTED); + + /** + * Indicates this is a network that has the ability to reach the + * carrier's MMSC for sending and receiving MMS messages. + */ + public static final int NET_CAPABILITY_MMS = 0; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * SUPL server, used to retrieve GPS information. + */ + public static final int NET_CAPABILITY_SUPL = 1; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * DUN or tethering gateway. + */ + public static final int NET_CAPABILITY_DUN = 2; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * FOTA portal, used for over the air updates. + */ + public static final int NET_CAPABILITY_FOTA = 3; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * IMS servers, used for network registration and signaling. + */ + public static final int NET_CAPABILITY_IMS = 4; + + /** + * Indicates this is a network that has the ability to reach the carrier's + * CBS servers, used for carrier specific services. + */ + public static final int NET_CAPABILITY_CBS = 5; + + /** + * Indicates this is a network that has the ability to reach a Wi-Fi direct + * peer. + */ + public static final int NET_CAPABILITY_WIFI_P2P = 6; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * Initial Attach servers. + */ + public static final int NET_CAPABILITY_IA = 7; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * RCS servers, used for Rich Communication Services. + */ + public static final int NET_CAPABILITY_RCS = 8; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * XCAP servers, used for configuration and control. + */ + public static final int NET_CAPABILITY_XCAP = 9; + + /** + * Indicates this is a network that has the ability to reach a carrier's + * Emergency IMS servers, used for network signaling during emergency calls. + */ + public static final int NET_CAPABILITY_EIMS = 10; + + /** + * Indicates that this network is unmetered. + */ + public static final int NET_CAPABILITY_NOT_METERED = 11; + + /** + * Indicates that this network should be able to reach the internet. + */ + public static final int NET_CAPABILITY_INTERNET = 12; + + /** + * Indicates that this network is available for general use. If this is not set + * applications should not attempt to communicate on this network. Note that this + * is simply informative and not enforcement - enforcement is handled via other means. + * Set by default. + */ + public static final int NET_CAPABILITY_NOT_RESTRICTED = 13; + + private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS; + private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_RESTRICTED; + + /** + * Adds the given capability to this {@code NetworkCapability} instance. + * Multiple capabilities may be applied sequentially. Note that when searching + * for a network to satisfy a request, all capabilities requested must be satisfied. + * + * @param networkCapability the {@code NetworkCapabilities.NET_CAPABILITY_*} to be added. + */ + public void addNetworkCapability(int networkCapability) { + if (networkCapability < MIN_NET_CAPABILITY || + networkCapability > MAX_NET_CAPABILITY) { + throw new IllegalArgumentException("NetworkCapability out of range"); + } + mNetworkCapabilities |= 1 << networkCapability; + } + + /** + * Removes (if found) the given capability from this {@code NetworkCapability} instance. + * + * @param networkCapability the {@code NetworkCapabilities.NET_CAPABILTIY_*} to be removed. + */ + public void removeNetworkCapability(int networkCapability) { + if (networkCapability < MIN_NET_CAPABILITY || + networkCapability > MAX_NET_CAPABILITY) { + throw new IllegalArgumentException("NetworkCapability out of range"); + } + mNetworkCapabilities &= ~(1 << networkCapability); + } + + /** + * Gets all the capabilities set on this {@code NetworkCapability} instance. + * + * @return a {@link Collection} of {@code NetworkCapabilities.NET_CAPABILITY_*} values + * for this instance. + */ + public Collection<Integer> getNetworkCapabilities() { + return enumerateBits(mNetworkCapabilities); + } + + /** + * Tests for the presence of a capabilitity on this instance. + * + * @param networkCapability the {@code NetworkCapabilities.NET_CAPABILITY_*} to be tested for. + * @return {@code true} if set on this instance. + */ + public boolean hasCapability(int networkCapability) { + if (networkCapability < MIN_NET_CAPABILITY || + networkCapability > MAX_NET_CAPABILITY) { + return false; + } + return ((mNetworkCapabilities & (1 << networkCapability)) != 0); + } + + private Collection<Integer> enumerateBits(long val) { + ArrayList<Integer> result = new ArrayList<Integer>(); + int resource = 0; + while (val > 0) { + if ((val & 1) == 1) result.add(resource); + val = val >> 1; + resource++; + } + return result; + } + + private void combineNetCapabilities(NetworkCapabilities nc) { + this.mNetworkCapabilities |= nc.mNetworkCapabilities; + } + + private boolean satisfiedByNetCapabilities(NetworkCapabilities nc) { + return ((nc.mNetworkCapabilities & this.mNetworkCapabilities) == this.mNetworkCapabilities); + } + + private boolean equalsNetCapabilities(NetworkCapabilities nc) { + return (nc.mNetworkCapabilities == this.mNetworkCapabilities); + } + + /** + * Representing the transport type. Apps should generally not care about transport. A + * request for a fast internet connection could be satisfied by a number of different + * transports. If any are specified here it will be satisfied a Network that matches + * any of them. If a caller doesn't care about the transport it should not specify any. + */ + private long mTransportTypes; + + /** + * Indicates this network uses a Cellular transport. + */ + public static final int TRANSPORT_CELLULAR = 0; + + /** + * Indicates this network uses a Wi-Fi transport. + */ + public static final int TRANSPORT_WIFI = 1; + + /** + * Indicates this network uses a Bluetooth transport. + */ + public static final int TRANSPORT_BLUETOOTH = 2; + + /** + * Indicates this network uses an Ethernet transport. + */ + public static final int TRANSPORT_ETHERNET = 3; + + private static final int MIN_TRANSPORT = TRANSPORT_CELLULAR; + private static final int MAX_TRANSPORT = TRANSPORT_ETHERNET; + + /** + * Adds the given transport type to this {@code NetworkCapability} instance. + * Multiple transports may be applied sequentially. Note that when searching + * for a network to satisfy a request, any listed in the request will satisfy the request. + * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a + * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network + * to be selected. This is logically different than + * {@code NetworkCapabilities.NET_CAPABILITY_*} listed above. + * + * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be added. + */ + public void addTransportType(int transportType) { + if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) { + throw new IllegalArgumentException("TransportType out of range"); + } + mTransportTypes |= 1 << transportType; + } + + /** + * Removes (if found) the given transport from this {@code NetworkCapability} instance. + * + * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be removed. + */ + public void removeTransportType(int transportType) { + if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) { + throw new IllegalArgumentException("TransportType out of range"); + } + mTransportTypes &= ~(1 << transportType); + } + + /** + * Gets all the transports set on this {@code NetworkCapability} instance. + * + * @return a {@link Collection} of {@code NetworkCapabilities.TRANSPORT_*} values + * for this instance. + */ + public Collection<Integer> getTransportTypes() { + return enumerateBits(mTransportTypes); + } + + /** + * Tests for the presence of a transport on this instance. + * + * @param transportType the {@code NetworkCapabilities.TRANSPORT_*} to be tested for. + * @return {@code true} if set on this instance. + */ + public boolean hasTransport(int transportType) { + if (transportType < MIN_TRANSPORT || transportType > MAX_TRANSPORT) { + return false; + } + return ((mTransportTypes & (1 << transportType)) != 0); + } + + private void combineTransportTypes(NetworkCapabilities nc) { + this.mTransportTypes |= nc.mTransportTypes; + } + private boolean satisfiedByTransportTypes(NetworkCapabilities nc) { + return ((this.mTransportTypes == 0) || + ((this.mTransportTypes & nc.mTransportTypes) != 0)); + } + private boolean equalsTransportTypes(NetworkCapabilities nc) { + return (nc.mTransportTypes == this.mTransportTypes); + } + + /** + * Passive link bandwidth. This is a rough guide of the expected peak bandwidth + * for the first hop on the given transport. It is not measured, but may take into account + * link parameters (Radio technology, allocated channels, etc). + */ + private int mLinkUpBandwidthKbps; + private int mLinkDownBandwidthKbps; + + /** + * Sets the upstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * Note that when used to request a network, this specifies the minimum acceptable. + * When received as the state of an existing network this specifies the typical + * first hop bandwidth expected. This is never measured, but rather is inferred + * from technology type and other link parameters. It could be used to differentiate + * between very slow 1xRTT cellular links and other faster networks or even between + * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between + * fast backhauls and slow backhauls. + * + * @param upKbps the estimated first hop upstream (device to network) bandwidth. + */ + public void setLinkUpstreamBandwidthKbps(int upKbps) { + mLinkUpBandwidthKbps = upKbps; + } + + /** + * Retrieves the upstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * + * @return The estimated first hop upstream (device to network) bandwidth. + */ + public int getLinkUpstreamBandwidthKbps() { + return mLinkUpBandwidthKbps; + } + + /** + * Sets the downstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * <p> + * Note that when used to request a network, this specifies the minimum acceptable. + * When received as the state of an existing network this specifies the typical + * first hop bandwidth expected. This is never measured, but rather is inferred + * from technology type and other link parameters. It could be used to differentiate + * between very slow 1xRTT cellular links and other faster networks or even between + * 802.11b vs 802.11AC wifi technologies. It should not be used to differentiate between + * fast backhauls and slow backhauls. + * + * @param downKbps the estimated first hop downstream (network to device) bandwidth. + */ + public void setLinkDownstreamBandwidthKbps(int downKbps) { + mLinkDownBandwidthKbps = downKbps; + } + + /** + * Retrieves the downstream bandwidth for this network in Kbps. This always only refers to + * the estimated first hop transport bandwidth. + * + * @return The estimated first hop downstream (network to device) bandwidth. + */ + public int getLinkDownstreamBandwidthKbps() { + return mLinkDownBandwidthKbps; + } + + private void combineLinkBandwidths(NetworkCapabilities nc) { + this.mLinkUpBandwidthKbps = + Math.max(this.mLinkUpBandwidthKbps, nc.mLinkUpBandwidthKbps); + this.mLinkDownBandwidthKbps = + Math.max(this.mLinkDownBandwidthKbps, nc.mLinkDownBandwidthKbps); + } + private boolean satisfiedByLinkBandwidths(NetworkCapabilities nc) { + return !(this.mLinkUpBandwidthKbps > nc.mLinkUpBandwidthKbps || + this.mLinkDownBandwidthKbps > nc.mLinkDownBandwidthKbps); + } + private boolean equalsLinkBandwidths(NetworkCapabilities nc) { + return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps && + this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps); + } + + /** + * Combine a set of Capabilities to this one. Useful for coming up with the complete set + * {@hide} + */ + public void combineCapabilities(NetworkCapabilities nc) { + combineNetCapabilities(nc); + combineTransportTypes(nc); + combineLinkBandwidths(nc); + } + + /** + * Check if our requirements are satisfied by the given Capabilities. + * {@hide} + */ + public boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc) { + return (nc != null && + satisfiedByNetCapabilities(nc) && + satisfiedByTransportTypes(nc) && + satisfiedByLinkBandwidths(nc)); + } + + @Override + public boolean equals(Object obj) { + if (obj == null || (obj instanceof NetworkCapabilities == false)) return false; + NetworkCapabilities that = (NetworkCapabilities)obj; + return (equalsNetCapabilities(that) && + equalsTransportTypes(that) && + equalsLinkBandwidths(that)); + } + + @Override + public int hashCode() { + return ((int)(mNetworkCapabilities & 0xFFFFFFFF) + + ((int)(mNetworkCapabilities >> 32) * 3) + + ((int)(mTransportTypes & 0xFFFFFFFF) * 5) + + ((int)(mTransportTypes >> 32) * 7) + + (mLinkUpBandwidthKbps * 11) + + (mLinkDownBandwidthKbps * 13)); + } + + public int describeContents() { + return 0; + } + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mNetworkCapabilities); + dest.writeLong(mTransportTypes); + dest.writeInt(mLinkUpBandwidthKbps); + dest.writeInt(mLinkDownBandwidthKbps); + } + public static final Creator<NetworkCapabilities> CREATOR = + new Creator<NetworkCapabilities>() { + public NetworkCapabilities createFromParcel(Parcel in) { + NetworkCapabilities netCap = new NetworkCapabilities(); + + netCap.mNetworkCapabilities = in.readLong(); + netCap.mTransportTypes = in.readLong(); + netCap.mLinkUpBandwidthKbps = in.readInt(); + netCap.mLinkDownBandwidthKbps = in.readInt(); + return netCap; + } + public NetworkCapabilities[] newArray(int size) { + return new NetworkCapabilities[size]; + } + }; + + public String toString() { + Collection<Integer> types = getTransportTypes(); + String transports = (types.size() > 0 ? " Transports: " : ""); + Iterator<Integer> i = types.iterator(); + while (i.hasNext()) { + switch (i.next()) { + case TRANSPORT_CELLULAR: transports += "CELLULAR"; break; + case TRANSPORT_WIFI: transports += "WIFI"; break; + case TRANSPORT_BLUETOOTH: transports += "BLUETOOTH"; break; + case TRANSPORT_ETHERNET: transports += "ETHERNET"; break; + } + if (i.hasNext()) transports += "|"; + } + + types = getNetworkCapabilities(); + String capabilities = (types.size() > 0 ? " Capabilities: " : ""); + i = types.iterator(); + while (i.hasNext()) { + switch (i.next().intValue()) { + case NET_CAPABILITY_MMS: capabilities += "MMS"; break; + case NET_CAPABILITY_SUPL: capabilities += "SUPL"; break; + case NET_CAPABILITY_DUN: capabilities += "DUN"; break; + case NET_CAPABILITY_FOTA: capabilities += "FOTA"; break; + case NET_CAPABILITY_IMS: capabilities += "IMS"; break; + case NET_CAPABILITY_CBS: capabilities += "CBS"; break; + case NET_CAPABILITY_WIFI_P2P: capabilities += "WIFI_P2P"; break; + case NET_CAPABILITY_IA: capabilities += "IA"; break; + case NET_CAPABILITY_RCS: capabilities += "RCS"; break; + case NET_CAPABILITY_XCAP: capabilities += "XCAP"; break; + case NET_CAPABILITY_EIMS: capabilities += "EIMS"; break; + case NET_CAPABILITY_NOT_METERED: capabilities += "NOT_METERED"; break; + case NET_CAPABILITY_INTERNET: capabilities += "INTERNET"; break; + case NET_CAPABILITY_NOT_RESTRICTED: capabilities += "NOT_RESTRICTED"; break; + } + if (i.hasNext()) capabilities += "&"; + } + + String upBand = ((mLinkUpBandwidthKbps > 0) ? " LinkUpBandwidth>=" + + mLinkUpBandwidthKbps + "Kbps" : ""); + String dnBand = ((mLinkDownBandwidthKbps > 0) ? " LinkDnBandwidth>=" + + mLinkDownBandwidthKbps + "Kbps" : ""); + + return "[" + transports + capabilities + upBand + dnBand + "]"; + } +} diff --git a/core/java/android/net/NetworkFactory.java b/core/java/android/net/NetworkFactory.java new file mode 100644 index 0000000..a20e8e7 --- /dev/null +++ b/core/java/android/net/NetworkFactory.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.net; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.Messenger; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; + +import com.android.internal.util.AsyncChannel; +import com.android.internal.util.Protocol; + +/** + * A NetworkFactory is an entity that creates NetworkAgent objects. + * The bearers register with ConnectivityService using {@link #register} and + * their factory will start receiving scored NetworkRequests. NetworkRequests + * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by + * overridden function. All of these can be dynamic - changing NetworkCapabilities + * or score forces re-evaluation of all current requests. + * + * If any requests pass the filter some overrideable functions will be called. + * If the bearer only cares about very simple start/stopNetwork callbacks, those + * functions can be overridden. If the bearer needs more interaction, it can + * override addNetworkRequest and removeNetworkRequest which will give it each + * request that passes their current filters. + * @hide + **/ +public class NetworkFactory extends Handler { + private static final boolean DBG = true; + + private static final int BASE = Protocol.BASE_NETWORK_FACTORY; + /** + * Pass a network request to the bearer. If the bearer believes it can + * satisfy the request it should connect to the network and create a + * NetworkAgent. Once the NetworkAgent is fully functional it will + * register itself with ConnectivityService using registerNetworkAgent. + * If the bearer cannot immediately satisfy the request (no network, + * user disabled the radio, lower-scored network) it should remember + * any NetworkRequests it may be able to satisfy in the future. It may + * disregard any that it will never be able to service, for example + * those requiring a different bearer. + * msg.obj = NetworkRequest + * msg.arg1 = score - the score of the any network currently satisfying this + * request. If this bearer knows in advance it cannot + * exceed this score it should not try to connect, holding the request + * for the future. + * Note that subsequent events may give a different (lower + * or higher) score for this request, transmitted to each + * NetworkFactory through additional CMD_REQUEST_NETWORK msgs + * with the same NetworkRequest but an updated score. + * Also, network conditions may change for this bearer + * allowing for a better score in the future. + */ + public static final int CMD_REQUEST_NETWORK = BASE; + + /** + * Cancel a network request + * msg.obj = NetworkRequest + */ + public static final int CMD_CANCEL_REQUEST = BASE + 1; + + /** + * Internally used to set our best-guess score. + * msg.arg1 = new score + */ + private static final int CMD_SET_SCORE = BASE + 2; + + /** + * Internally used to set our current filter for coarse bandwidth changes with + * technology changes. + * msg.obj = new filter + */ + private static final int CMD_SET_FILTER = BASE + 3; + + private final Context mContext; + private final String LOG_TAG; + + private final SparseArray<NetworkRequestInfo> mNetworkRequests = + new SparseArray<NetworkRequestInfo>(); + + private int mScore; + private NetworkCapabilities mCapabilityFilter; + + private int mRefCount = 0; + private Messenger mMessenger = null; + + public NetworkFactory(Looper looper, Context context, String logTag, + NetworkCapabilities filter) { + super(looper); + LOG_TAG = logTag; + mContext = context; + mCapabilityFilter = filter; + } + + public void register() { + if (DBG) log("Registering NetworkFactory"); + if (mMessenger == null) { + mMessenger = new Messenger(this); + ConnectivityManager.from(mContext).registerNetworkFactory(mMessenger, LOG_TAG); + } + } + + public void unregister() { + if (DBG) log("Unregistering NetworkFactory"); + if (mMessenger != null) { + ConnectivityManager.from(mContext).unregisterNetworkFactory(mMessenger); + mMessenger = null; + } + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CMD_REQUEST_NETWORK: { + handleAddRequest((NetworkRequest)msg.obj, msg.arg1); + break; + } + case CMD_CANCEL_REQUEST: { + handleRemoveRequest((NetworkRequest) msg.obj); + break; + } + case CMD_SET_SCORE: { + handleSetScore(msg.arg1); + break; + } + case CMD_SET_FILTER: { + handleSetFilter((NetworkCapabilities) msg.obj); + break; + } + } + } + + private class NetworkRequestInfo { + public final NetworkRequest request; + public int score; + public boolean requested; // do we have a request outstanding, limited by score + + public NetworkRequestInfo(NetworkRequest request, int score) { + this.request = request; + this.score = score; + this.requested = false; + } + } + + private void handleAddRequest(NetworkRequest request, int score) { + NetworkRequestInfo n = mNetworkRequests.get(request.requestId); + if (n == null) { + n = new NetworkRequestInfo(request, score); + mNetworkRequests.put(n.request.requestId, n); + } else { + n.score = score; + } + if (DBG) log("got request " + request + " with score " + score); + if (DBG) log(" my score=" + mScore + ", my filter=" + mCapabilityFilter); + + evalRequest(n); + } + + private void handleRemoveRequest(NetworkRequest request) { + NetworkRequestInfo n = mNetworkRequests.get(request.requestId); + if (n != null && n.requested) { + mNetworkRequests.remove(request.requestId); + releaseNetworkFor(n.request); + } + } + + private void handleSetScore(int score) { + mScore = score; + evalRequests(); + } + + private void handleSetFilter(NetworkCapabilities netCap) { + mCapabilityFilter = netCap; + evalRequests(); + } + + /** + * Overridable function to provide complex filtering. + * Called for every request every time a new NetworkRequest is seen + * and whenever the filterScore or filterNetworkCapabilities change. + * + * acceptRequest can be overriden to provide complex filter behavior + * for the incoming requests + * + * For output, this class will call {@link #needNetworkFor} and + * {@link #releaseNetworkFor} for every request that passes the filters. + * If you don't need to see every request, you can leave the base + * implementations of those two functions and instead override + * {@link #startNetwork} and {@link #stopNetwork}. + * + * If you want to see every score fluctuation on every request, set + * your score filter to a very high number and watch {@link #needNetworkFor}. + * + * @return {@code true} to accept the request. + */ + public boolean acceptRequest(NetworkRequest request, int score) { + return true; + } + + private void evalRequest(NetworkRequestInfo n) { + if (n.requested == false && n.score < mScore && + n.request.networkCapabilities.satisfiedByNetworkCapabilities( + mCapabilityFilter) && acceptRequest(n.request, n.score)) { + needNetworkFor(n.request, n.score); + n.requested = true; + } else if (n.requested == true && + (n.score > mScore || n.request.networkCapabilities.satisfiedByNetworkCapabilities( + mCapabilityFilter) == false || acceptRequest(n.request, n.score) == false)) { + releaseNetworkFor(n.request); + n.requested = false; + } + } + + private void evalRequests() { + for (int i = 0; i < mNetworkRequests.size(); i++) { + NetworkRequestInfo n = mNetworkRequests.valueAt(i); + + evalRequest(n); + } + } + + // override to do simple mode (request independent) + protected void startNetwork() { } + protected void stopNetwork() { } + + // override to do fancier stuff + protected void needNetworkFor(NetworkRequest networkRequest, int score) { + if (++mRefCount == 1) startNetwork(); + } + + protected void releaseNetworkFor(NetworkRequest networkRequest) { + if (--mRefCount == 0) stopNetwork(); + } + + + public void addNetworkRequest(NetworkRequest networkRequest, int score) { + sendMessage(obtainMessage(CMD_REQUEST_NETWORK, + new NetworkRequestInfo(networkRequest, score))); + } + + public void removeNetworkRequest(NetworkRequest networkRequest) { + sendMessage(obtainMessage(CMD_CANCEL_REQUEST, networkRequest)); + } + + public void setScoreFilter(int score) { + sendMessage(obtainMessage(CMD_SET_SCORE, score, 0)); + } + + public void setCapabilityFilter(NetworkCapabilities netCap) { + sendMessage(obtainMessage(CMD_SET_FILTER, new NetworkCapabilities(netCap))); + } + + protected void log(String s) { + Log.d(LOG_TAG, s); + } +} diff --git a/core/java/android/net/NetworkInfo.java b/core/java/android/net/NetworkInfo.java index 53b1308..d279412 100644 --- a/core/java/android/net/NetworkInfo.java +++ b/core/java/android/net/NetworkInfo.java @@ -188,6 +188,15 @@ public class NetworkInfo implements Parcelable { } /** + * @hide + */ + public void setType(int type) { + synchronized (this) { + mNetworkType = type; + } + } + + /** * Return a network-type-specific integer describing the subtype * of the network. * @return the network subtype @@ -198,7 +207,10 @@ public class NetworkInfo implements Parcelable { } } - void setSubtype(int subtype, String subtypeName) { + /** + * @hide + */ + public void setSubtype(int subtype, String subtypeName) { synchronized (this) { mSubtype = subtype; mSubtypeName = subtypeName; @@ -420,7 +432,7 @@ public class NetworkInfo implements Parcelable { @Override public String toString() { synchronized (this) { - StringBuilder builder = new StringBuilder("NetworkInfo: "); + StringBuilder builder = new StringBuilder("["); builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()). append("], state: ").append(mState).append("/").append(mDetailedState). append(", reason: ").append(mReason == null ? "(unspecified)" : mReason). @@ -429,7 +441,8 @@ public class NetworkInfo implements Parcelable { append(", failover: ").append(mIsFailover). append(", isAvailable: ").append(mIsAvailable). append(", isConnectedToProvisioningNetwork: "). - append(mIsConnectedToProvisioningNetwork); + append(mIsConnectedToProvisioningNetwork). + append("]"); return builder.toString(); } } diff --git a/core/java/android/net/NetworkRequest.aidl b/core/java/android/net/NetworkRequest.aidl new file mode 100644 index 0000000..508defc --- /dev/null +++ b/core/java/android/net/NetworkRequest.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.net; + +parcelable NetworkRequest; + diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java new file mode 100644 index 0000000..47377e9 --- /dev/null +++ b/core/java/android/net/NetworkRequest.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 android.net; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Defines a request for a network, made by calling {@link ConnectivityManager#requestNetwork} + * or {@link ConnectivityManager#listenForNetwork}. + * + * This token records the {@link NetworkCapabilities} used to make the request and identifies + * the request. It should be used to release the request via + * {@link ConnectivityManager#releaseNetworkRequest} when the network is no longer desired. + */ +public class NetworkRequest implements Parcelable { + /** + * The {@link NetworkCapabilities} that define this request. This should not be modified. + * The networkCapabilities of the request are set when + * {@link ConnectivityManager#requestNetwork} is called and the value is presented here + * as a convenient reminder of what was requested. + */ + public final NetworkCapabilities networkCapabilities; + + /** + * Identifies the request. NetworkRequests should only be constructed by + * the Framework and given out to applications as tokens to be used to identify + * the request. + * @hide + */ + public final int requestId; + + /** + * Set for legacy requests and the default. Set to TYPE_NONE for none. + * Causes CONNECTIVITY_ACTION broadcasts to be sent. + * @hide + */ + public final int legacyType; + + /** + * @hide + */ + public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId) { + requestId = rId; + networkCapabilities = nc; + this.legacyType = legacyType; + } + + /** + * @hide + */ + public NetworkRequest(NetworkRequest that) { + networkCapabilities = new NetworkCapabilities(that.networkCapabilities); + requestId = that.requestId; + this.legacyType = that.legacyType; + } + + // implement the Parcelable interface + public int describeContents() { + return 0; + } + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(networkCapabilities, flags); + dest.writeInt(legacyType); + dest.writeInt(requestId); + } + public static final Creator<NetworkRequest> CREATOR = + new Creator<NetworkRequest>() { + public NetworkRequest createFromParcel(Parcel in) { + NetworkCapabilities nc = (NetworkCapabilities)in.readParcelable(null); + int legacyType = in.readInt(); + int requestId = in.readInt(); + NetworkRequest result = new NetworkRequest(nc, legacyType, requestId); + return result; + } + public NetworkRequest[] newArray(int size) { + return new NetworkRequest[size]; + } + }; + + public String toString() { + return "NetworkRequest [ id=" + requestId + ", legacyType=" + legacyType + + ", " + networkCapabilities.toString() + " ]"; + } + + public boolean equals(Object obj) { + if (obj instanceof NetworkRequest == false) return false; + NetworkRequest that = (NetworkRequest)obj; + return (that.legacyType == this.legacyType && + that.requestId == this.requestId && + ((that.networkCapabilities == null && this.networkCapabilities == null) || + (that.networkCapabilities != null && + that.networkCapabilities.equals(this.networkCapabilities)))); + } + + public int hashCode() { + return requestId + (legacyType * 1013) + + (networkCapabilities.hashCode() * 1051); + } +} diff --git a/core/java/android/net/NetworkState.java b/core/java/android/net/NetworkState.java index fbe1f82..2e0e9e4 100644 --- a/core/java/android/net/NetworkState.java +++ b/core/java/android/net/NetworkState.java @@ -28,21 +28,21 @@ public class NetworkState implements Parcelable { public final NetworkInfo networkInfo; public final LinkProperties linkProperties; - public final LinkCapabilities linkCapabilities; + public final NetworkCapabilities networkCapabilities; /** Currently only used by testing. */ public final String subscriberId; public final String networkId; public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties, - LinkCapabilities linkCapabilities) { - this(networkInfo, linkProperties, linkCapabilities, null, null); + NetworkCapabilities networkCapabilities) { + this(networkInfo, linkProperties, networkCapabilities, null, null); } public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties, - LinkCapabilities linkCapabilities, String subscriberId, String networkId) { + NetworkCapabilities networkCapabilities, String subscriberId, String networkId) { this.networkInfo = networkInfo; this.linkProperties = linkProperties; - this.linkCapabilities = linkCapabilities; + this.networkCapabilities = networkCapabilities; this.subscriberId = subscriberId; this.networkId = networkId; } @@ -50,7 +50,7 @@ public class NetworkState implements Parcelable { public NetworkState(Parcel in) { networkInfo = in.readParcelable(null); linkProperties = in.readParcelable(null); - linkCapabilities = in.readParcelable(null); + networkCapabilities = in.readParcelable(null); subscriberId = in.readString(); networkId = in.readString(); } @@ -64,7 +64,7 @@ public class NetworkState implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeParcelable(networkInfo, flags); out.writeParcelable(linkProperties, flags); - out.writeParcelable(linkCapabilities, flags); + out.writeParcelable(networkCapabilities, flags); out.writeString(subscriberId); out.writeString(networkId); } diff --git a/core/java/android/net/NetworkStateTracker.java b/core/java/android/net/NetworkStateTracker.java index c49b1d1..35500cc 100644 --- a/core/java/android/net/NetworkStateTracker.java +++ b/core/java/android/net/NetworkStateTracker.java @@ -111,12 +111,9 @@ public interface NetworkStateTracker { public LinkProperties getLinkProperties(); /** - * A capability is an Integer/String pair, the capabilities - * are defined in the class LinkSocket#Key. - * * @return a copy of this connections capabilities, may be empty but never null. */ - public LinkCapabilities getLinkCapabilities(); + public NetworkCapabilities getNetworkCapabilities(); /** * Get interesting information about this network link @@ -250,4 +247,14 @@ public interface NetworkStateTracker { */ public void stopSampling(SamplingDataTracker.SamplingSnapshot s); + /* + * Record the current netId + */ + public void setNetId(int netId); + + /* + * ? + */ + public Network getNetwork(); + } diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java index b24d396..edb3286 100644 --- a/core/java/android/net/NetworkUtils.java +++ b/core/java/android/net/NetworkUtils.java @@ -109,6 +109,50 @@ public class NetworkUtils { public native static void markSocket(int socketfd, int mark); /** + * Binds the current process to the network designated by {@code netId}. All sockets created + * in the future (and not explicitly bound via a bound {@link SocketFactory} (see + * {@link Network#socketFactory}) will be bound to this network. Note that if this + * {@code Network} ever disconnects all sockets created in this way will cease to work. This + * is by design so an application doesn't accidentally use sockets it thinks are still bound to + * a particular {@code Network}. + */ + public native static void bindProcessToNetwork(int netId); + + /** + * Clear any process specific {@code Network} binding. This reverts a call to + * {@link #bindProcessToNetwork}. + */ + public native static void unbindProcessToNetwork(); + + /** + * Return the netId last passed to {@link #bindProcessToNetwork}, or NETID_UNSET if + * {@link #unbindProcessToNetwork} has been called since {@link #bindProcessToNetwork}. + */ + public native static int getNetworkBoundToProcess(); + + /** + * Binds host resolutions performed by this process to the network designated by {@code netId}. + * {@link #bindProcessToNetwork} takes precedence over this setting. + * + * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature(). + */ + public native static void bindProcessToNetworkForHostResolution(int netId); + + /** + * Clears any process specific {@link Network} binding for host resolution. This does + * not clear bindings enacted via {@link #bindProcessToNetwork}. + * + * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature(). + */ + public native static void unbindProcessToNetworkForHostResolution(); + + /** + * Explicitly binds {@code socketfd} to the network designated by {@code netId}. This + * overrides any binding via {@link #bindProcessToNetwork}. + */ + public native static void bindSocketToNetwork(int socketfd, int netId); + + /** * Convert a IPv4 address from an integer to an InetAddress. * @param hostAddress an int corresponding to the IPv4 address in network byte order */ diff --git a/core/java/android/net/Proxy.java b/core/java/android/net/Proxy.java index 8f41e85..6a78c29 100644 --- a/core/java/android/net/Proxy.java +++ b/core/java/android/net/Proxy.java @@ -273,19 +273,19 @@ public final class Proxy { String host = null; String port = null; String exclList = null; - String pacFileUrl = null; + Uri pacFileUrl = Uri.EMPTY; if (p != null) { host = p.getHost(); port = Integer.toString(p.getPort()); exclList = p.getExclusionListAsString(); - pacFileUrl = p.getPacFileUrl().toString(); + pacFileUrl = p.getPacFileUrl(); } setHttpProxySystemProperty(host, port, exclList, pacFileUrl); } /** @hide */ public static final void setHttpProxySystemProperty(String host, String port, String exclList, - String pacFileUrl) { + Uri pacFileUrl) { if (exclList != null) exclList = exclList.replace(",", "|"); if (false) Log.d(TAG, "setHttpProxySystemProperty :"+host+":"+port+" - "+exclList); if (host != null) { @@ -309,7 +309,7 @@ public final class Proxy { System.clearProperty("http.nonProxyHosts"); System.clearProperty("https.nonProxyHosts"); } - if (!TextUtils.isEmpty(pacFileUrl)) { + if (!Uri.EMPTY.equals(pacFileUrl)) { ProxySelector.setDefault(new PacProxySelector()); } else { ProxySelector.setDefault(sDefaultProxySelector); diff --git a/core/java/android/net/ProxyDataTracker.java b/core/java/android/net/ProxyDataTracker.java index 461e8b8..4973b3d 100644 --- a/core/java/android/net/ProxyDataTracker.java +++ b/core/java/android/net/ProxyDataTracker.java @@ -104,7 +104,7 @@ public class ProxyDataTracker extends BaseNetworkStateTracker { public ProxyDataTracker() { mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_PROXY, 0, NETWORK_TYPE, ""); mLinkProperties = new LinkProperties(); - mLinkCapabilities = new LinkCapabilities(); + mNetworkCapabilities = new NetworkCapabilities(); mNetworkInfo.setIsAvailable(true); try { mLinkProperties.addDns(InetAddress.getByName(DNS1)); diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java index b40941f..7ea6bae 100644 --- a/core/java/android/net/ProxyInfo.java +++ b/core/java/android/net/ProxyInfo.java @@ -44,7 +44,7 @@ public class ProxyInfo implements Parcelable { private String mExclusionList; private String[] mParsedExclusionList; - private String mPacFileUrl; + private Uri mPacFileUrl; /** *@hide */ @@ -85,7 +85,7 @@ public class ProxyInfo implements Parcelable { * at the specified URL. */ public static ProxyInfo buildPacProxy(Uri pacUri) { - return new ProxyInfo(pacUri.toString()); + return new ProxyInfo(pacUri); } /** @@ -96,27 +96,45 @@ public class ProxyInfo implements Parcelable { mHost = host; mPort = port; setExclusionList(exclList); + mPacFileUrl = Uri.EMPTY; } /** * Create a ProxyProperties that points at a PAC URL. * @hide */ - public ProxyInfo(String pacFileUrl) { + public ProxyInfo(Uri pacFileUrl) { mHost = LOCAL_HOST; mPort = LOCAL_PORT; setExclusionList(LOCAL_EXCL_LIST); + if (pacFileUrl == null) { + throw new NullPointerException(); + } mPacFileUrl = pacFileUrl; } /** + * Create a ProxyProperties that points at a PAC URL. + * @hide + */ + public ProxyInfo(String pacFileUrl) { + mHost = LOCAL_HOST; + mPort = LOCAL_PORT; + setExclusionList(LOCAL_EXCL_LIST); + mPacFileUrl = Uri.parse(pacFileUrl); + } + + /** * Only used in PacManager after Local Proxy is bound. * @hide */ - public ProxyInfo(String pacFileUrl, int localProxyPort) { + public ProxyInfo(Uri pacFileUrl, int localProxyPort) { mHost = LOCAL_HOST; mPort = localProxyPort; setExclusionList(LOCAL_EXCL_LIST); + if (pacFileUrl == null) { + throw new NullPointerException(); + } mPacFileUrl = pacFileUrl; } @@ -125,7 +143,7 @@ public class ProxyInfo implements Parcelable { mPort = port; mExclusionList = exclList; mParsedExclusionList = parsedExclList; - mPacFileUrl = null; + mPacFileUrl = Uri.EMPTY; } // copy constructor instead of clone @@ -139,6 +157,8 @@ public class ProxyInfo implements Parcelable { mPacFileUrl = source.mPacFileUrl; mExclusionList = source.getExclusionListAsString(); mParsedExclusionList = source.mParsedExclusionList; + } else { + mPacFileUrl = Uri.EMPTY; } } @@ -158,10 +178,7 @@ public class ProxyInfo implements Parcelable { * no PAC script. */ public Uri getPacFileUrl() { - if (TextUtils.isEmpty(mPacFileUrl)) { - return null; - } - return Uri.parse(mPacFileUrl); + return mPacFileUrl; } /** @@ -210,7 +227,7 @@ public class ProxyInfo implements Parcelable { * @hide */ public boolean isValid() { - if (!TextUtils.isEmpty(mPacFileUrl)) return true; + if (!Uri.EMPTY.equals(mPacFileUrl)) return true; return Proxy.PROXY_VALID == Proxy.validate(mHost == null ? "" : mHost, mPort == 0 ? "" : Integer.toString(mPort), mExclusionList == null ? "" : mExclusionList); @@ -234,7 +251,7 @@ public class ProxyInfo implements Parcelable { @Override public String toString() { StringBuilder sb = new StringBuilder(); - if (mPacFileUrl != null) { + if (!Uri.EMPTY.equals(mPacFileUrl)) { sb.append("PAC Script: "); sb.append(mPacFileUrl); } else if (mHost != null) { @@ -257,13 +274,15 @@ public class ProxyInfo implements Parcelable { ProxyInfo p = (ProxyInfo)o; // If PAC URL is present in either then they must be equal. // Other parameters will only be for fall back. - if (!TextUtils.isEmpty(mPacFileUrl)) { + if (!Uri.EMPTY.equals(mPacFileUrl)) { return mPacFileUrl.equals(p.getPacFileUrl()) && mPort == p.mPort; } - if (!TextUtils.isEmpty(p.mPacFileUrl)) { + if (!Uri.EMPTY.equals(p.mPacFileUrl)) { + return false; + } + if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) { return false; } - if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) return false; if (mHost != null && p.getHost() != null && mHost.equals(p.getHost()) == false) { return false; } @@ -296,9 +315,9 @@ public class ProxyInfo implements Parcelable { * @hide */ public void writeToParcel(Parcel dest, int flags) { - if (mPacFileUrl != null) { + if (!Uri.EMPTY.equals(mPacFileUrl)) { dest.writeByte((byte)1); - dest.writeString(mPacFileUrl); + mPacFileUrl.writeToParcel(dest, 0); dest.writeInt(mPort); return; } else { @@ -325,7 +344,7 @@ public class ProxyInfo implements Parcelable { String host = null; int port = 0; if (in.readByte() != 0) { - String url = in.readString(); + Uri url = Uri.CREATOR.createFromParcel(in); int localPort = in.readInt(); return new ProxyInfo(url, localPort); } diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java index 1d051dd..ad8e4f7 100644 --- a/core/java/android/net/RouteInfo.java +++ b/core/java/android/net/RouteInfo.java @@ -25,22 +25,26 @@ import java.net.Inet4Address; import java.net.Inet6Address; import java.util.Collection; +import java.util.Objects; /** - * A simple container for route information. + * Represents a network route. + * <p> + * This is used both to describe static network configuration and live network + * configuration information. * - * In order to be used, a route must have a destination prefix and: - * - * - A gateway address (next-hop, for gatewayed routes), or - * - An interface (for directly-connected routes), or - * - Both a gateway and an interface. - * - * This class does not enforce these constraints because there is code that - * uses RouteInfo objects to store directly-connected routes without interfaces. - * Such objects cannot be used directly, but can be put into a LinkProperties - * object which then specifies the interface. - * - * @hide + * A route contains three pieces of information: + * <ul> + * <li>a destination {@link LinkAddress} for directly-connected subnets. If this is + * {@code null} it indicates a default route of the address family (IPv4 or IPv6) + * implied by the gateway IP address. + * <li>a gateway {@link InetAddress} for default routes. If this is {@code null} it + * indicates a directly-connected route. + * <li>an interface (which may be unspecified). + * </ul> + * Either the destination or the gateway may be {@code null}, but not both. If the + * destination and gateway are both specified, they must be of the same address family + * (IPv4 or IPv6). */ public class RouteInfo implements Parcelable { /** @@ -67,10 +71,10 @@ public class RouteInfo implements Parcelable { * * If destination is null, then gateway must be specified and the * constructed route is either the IPv4 default route <code>0.0.0.0</code> - * if @gateway is an instance of {@link Inet4Address}, or the IPv6 default + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default * route <code>::/0</code> if gateway is an instance of * {@link Inet6Address}. - * + * <p> * destination and gateway may not both be null. * * @param destination the destination prefix @@ -102,28 +106,64 @@ public class RouteInfo implements Parcelable { mDestination = new LinkAddress(NetworkUtils.getNetworkPart(destination.getAddress(), destination.getNetworkPrefixLength()), destination.getNetworkPrefixLength()); + if ((destination.getAddress() instanceof Inet4Address && + (gateway instanceof Inet4Address == false)) || + (destination.getAddress() instanceof Inet6Address && + (gateway instanceof Inet6Address == false))) { + throw new IllegalArgumentException("address family mismatch in RouteInfo constructor"); + } mGateway = gateway; mInterface = iface; mIsDefault = isDefault(); mIsHost = isHost(); } + /** + * Constructs a {@code RouteInfo} object. + * + * If destination is null, then gateway must be specified and the + * constructed route is either the IPv4 default route <code>0.0.0.0</code> + * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default + * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}. + * <p> + * Destination and gateway may not both be null. + * + * @param destination the destination address and prefix in a {@link LinkAddress} + * @param gateway the {@link InetAddress} to route packets through + */ public RouteInfo(LinkAddress destination, InetAddress gateway) { this(destination, gateway, null); } + /** + * Constructs a default {@code RouteInfo} object. + * + * @param gateway the {@link InetAddress} to route packets through + */ public RouteInfo(InetAddress gateway) { this(null, gateway, null); } - public RouteInfo(LinkAddress host) { - this(host, null, null); + /** + * Constructs a {@code RouteInfo} object representing a direct connected subnet. + * + * @param destination the {@link LinkAddress} describing the address and prefix + * length of the subnet. + */ + public RouteInfo(LinkAddress destination) { + this(destination, null, null); } + /** + * @hide + */ public static RouteInfo makeHostRoute(InetAddress host, String iface) { return makeHostRoute(host, null, iface); } + /** + * @hide + */ public static RouteInfo makeHostRoute(InetAddress host, InetAddress gateway, String iface) { if (host == null) return null; @@ -153,31 +193,108 @@ public class RouteInfo implements Parcelable { return val; } - + /** + * Retrieves the destination address and prefix length in the form of a {@link LinkAddress}. + * + * @return {@link LinkAddress} specifying the destination. This is never {@code null}. + */ public LinkAddress getDestination() { return mDestination; } + /** + * Retrieves the gateway or next hop {@link InetAddress} for this route. + * + * @return {@link InetAddress} specifying the gateway or next hop. This may be + & {@code null} for a directly-connected route." + */ public InetAddress getGateway() { return mGateway; } + /** + * Retrieves the interface used for this route if specified, else {@code null}. + * + * @return The name of the interface used for this route. + */ public String getInterface() { return mInterface; } + /** + * Indicates if this route is a default route (ie, has no destination specified). + * + * @return {@code true} if the destination has a prefix length of 0. + */ public boolean isDefaultRoute() { return mIsDefault; } + /** + * Indicates if this route is a host route (ie, matches only a single host address). + * + * @return {@code true} if the destination has a prefix length of 32/128 for v4/v6. + * @hide + */ public boolean isHostRoute() { return mIsHost; } + /** + * Indicates if this route has a next hop ({@code true}) or is directly-connected + * ({@code false}). + * + * @return {@code true} if a gateway is specified + * @hide + */ public boolean hasGateway() { return mHasGateway; } + /** + * Determines whether the destination and prefix of this route includes the specified + * address. + * + * @param destination A {@link InetAddress} to test to see if it would match this route. + * @return {@code true} if the destination and prefix length cover the given address. + */ + public boolean matches(InetAddress destination) { + if (destination == null) return false; + + // match the route destination and destination with prefix length + InetAddress dstNet = NetworkUtils.getNetworkPart(destination, + mDestination.getNetworkPrefixLength()); + + return mDestination.getAddress().equals(dstNet); + } + + /** + * Find the route from a Collection of routes that best matches a given address. + * May return null if no routes are applicable. + * @param routes a Collection of RouteInfos to chose from + * @param dest the InetAddress your trying to get to + * @return the RouteInfo from the Collection that best fits the given address + * + * @hide + */ + public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) { + if ((routes == null) || (dest == null)) return null; + + RouteInfo bestRoute = null; + // pick a longest prefix match under same address type + for (RouteInfo route : routes) { + if (NetworkUtils.addressTypeMatches(route.mDestination.getAddress(), dest)) { + if ((bestRoute != null) && + (bestRoute.mDestination.getNetworkPrefixLength() >= + route.mDestination.getNetworkPrefixLength())) { + continue; + } + if (route.matches(dest)) bestRoute = route; + } + } + return bestRoute; + } + public String toString() { String val = ""; if (mDestination != null) val = mDestination.toString(); @@ -185,10 +302,37 @@ public class RouteInfo implements Parcelable { return val; } + public boolean equals(Object obj) { + if (this == obj) return true; + + if (!(obj instanceof RouteInfo)) return false; + + RouteInfo target = (RouteInfo) obj; + + return Objects.equals(mDestination, target.getDestination()) && + Objects.equals(mGateway, target.getGateway()) && + Objects.equals(mInterface, target.getInterface()); + } + + public int hashCode() { + return (mDestination == null ? 0 : mDestination.hashCode() * 41) + + (mGateway == null ? 0 :mGateway.hashCode() * 47) + + (mInterface == null ? 0 :mInterface.hashCode() * 67) + + (mIsDefault ? 3 : 7); + } + + /** + * Implement the Parcelable interface + * @hide + */ public int describeContents() { return 0; } + /** + * Implement the Parcelable interface + * @hide + */ public void writeToParcel(Parcel dest, int flags) { if (mDestination == null) { dest.writeByte((byte) 0); @@ -208,38 +352,10 @@ public class RouteInfo implements Parcelable { dest.writeString(mInterface); } - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - - if (!(obj instanceof RouteInfo)) return false; - - RouteInfo target = (RouteInfo) obj; - - boolean sameDestination = ( mDestination == null) ? - target.getDestination() == null - : mDestination.equals(target.getDestination()); - - boolean sameAddress = (mGateway == null) ? - target.getGateway() == null - : mGateway.equals(target.getGateway()); - - boolean sameInterface = (mInterface == null) ? - target.getInterface() == null - : mInterface.equals(target.getInterface()); - - return sameDestination && sameAddress && sameInterface - && mIsDefault == target.mIsDefault; - } - - @Override - public int hashCode() { - return (mDestination == null ? 0 : mDestination.hashCode() * 41) - + (mGateway == null ? 0 :mGateway.hashCode() * 47) - + (mInterface == null ? 0 :mInterface.hashCode() * 67) - + (mIsDefault ? 3 : 7); - } - + /** + * Implement the Parcelable interface. + * @hide + */ public static final Creator<RouteInfo> CREATOR = new Creator<RouteInfo>() { public RouteInfo createFromParcel(Parcel in) { @@ -279,39 +395,4 @@ public class RouteInfo implements Parcelable { return new RouteInfo[size]; } }; - - protected boolean matches(InetAddress destination) { - if (destination == null) return false; - - // match the route destination and destination with prefix length - InetAddress dstNet = NetworkUtils.getNetworkPart(destination, - mDestination.getNetworkPrefixLength()); - - return mDestination.getAddress().equals(dstNet); - } - - /** - * Find the route from a Collection of routes that best matches a given address. - * May return null if no routes are applicable. - * @param routes a Collection of RouteInfos to chose from - * @param dest the InetAddress your trying to get to - * @return the RouteInfo from the Collection that best fits the given address - */ - public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) { - if ((routes == null) || (dest == null)) return null; - - RouteInfo bestRoute = null; - // pick a longest prefix match under same address type - for (RouteInfo route : routes) { - if (NetworkUtils.addressTypeMatches(route.mDestination.getAddress(), dest)) { - if ((bestRoute != null) && - (bestRoute.mDestination.getNetworkPrefixLength() >= - route.mDestination.getNetworkPrefixLength())) { - continue; - } - if (route.matches(dest)) bestRoute = route; - } - } - return bestRoute; - } } diff --git a/core/java/android/nfc/INfcAdapter.aidl b/core/java/android/nfc/INfcAdapter.aidl index 635a50f..9218c11 100644 --- a/core/java/android/nfc/INfcAdapter.aidl +++ b/core/java/android/nfc/INfcAdapter.aidl @@ -25,7 +25,6 @@ import android.nfc.IAppCallback; import android.nfc.INfcAdapterExtras; import android.nfc.INfcTag; import android.nfc.INfcCardEmulation; -import android.nfc.INfcUnlockSettings; import android.os.Bundle; /** @@ -36,7 +35,6 @@ interface INfcAdapter INfcTag getNfcTagInterface(); INfcCardEmulation getNfcCardEmulationInterface(); INfcAdapterExtras getNfcAdapterExtrasInterface(in String pkg); - INfcUnlockSettings getNfcUnlockSettingsInterface(); int getState(); boolean disable(boolean saveState); diff --git a/core/java/android/nfc/INfcUnlockSettings.aidl b/core/java/android/nfc/INfcUnlockSettings.aidl deleted file mode 100644 index 649eeed..0000000 --- a/core/java/android/nfc/INfcUnlockSettings.aidl +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 96a3947..dd8e41c 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -292,7 +292,6 @@ public final class NfcAdapter { static INfcAdapter sService; static INfcTag sTagService; static INfcCardEmulation sCardEmulationService; - static INfcUnlockSettings sNfcUnlockSettingsService; /** * The NfcAdapter object for each application context. @@ -433,13 +432,6 @@ 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) { @@ -557,22 +549,6 @@ 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 */ diff --git a/core/java/android/nfc/NfcUnlock.java b/core/java/android/nfc/NfcUnlock.java deleted file mode 100644 index 82dcd96..0000000 --- a/core/java/android/nfc/NfcUnlock.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * 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/cardemulation/AidGroup.java b/core/java/android/nfc/cardemulation/AidGroup.java index b0449224..cabda5d 100644 --- a/core/java/android/nfc/cardemulation/AidGroup.java +++ b/core/java/android/nfc/cardemulation/AidGroup.java @@ -2,6 +2,7 @@ package android.nfc.cardemulation; import java.io.IOException; import java.util.ArrayList; +import java.util.List; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -21,6 +22,8 @@ import android.util.Log; * <p>The format of AIDs is defined in the ISO/IEC 7816-4 specification. This class * requires the AIDs to be input as a hexadecimal string, with an even amount of * hexadecimal characters, e.g. "F014811481". + * + * @hide */ public final class AidGroup implements Parcelable { /** @@ -30,7 +33,7 @@ public final class AidGroup implements Parcelable { static final String TAG = "AidGroup"; - final ArrayList<String> aids; + final List<String> aids; final String category; final String description; @@ -40,7 +43,7 @@ public final class AidGroup implements Parcelable { * @param aids The list of AIDs present in the group * @param category The category of this group, e.g. {@link CardEmulation#CATEGORY_PAYMENT} */ - public AidGroup(ArrayList<String> aids, String category) { + public AidGroup(List<String> aids, String category) { if (aids == null || aids.size() == 0) { throw new IllegalArgumentException("No AIDS in AID group."); } @@ -72,7 +75,7 @@ public final class AidGroup implements Parcelable { /** * @return the list of AIDs in this group */ - public ArrayList<String> getAids() { + public List<String> getAids() { return aids; } @@ -121,11 +124,6 @@ public final class AidGroup implements Parcelable { } }; - /** - * @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>(); @@ -152,9 +150,6 @@ public final class AidGroup implements Parcelable { } } - /** - * @hide - */ public void writeAsXml(XmlSerializer out) throws IOException { out.attribute(null, "category", category); for (String aid : aids) { diff --git a/core/java/android/nfc/cardemulation/CardEmulation.java b/core/java/android/nfc/cardemulation/CardEmulation.java index e24a22a..4b9e890 100644 --- a/core/java/android/nfc/cardemulation/CardEmulation.java +++ b/core/java/android/nfc/cardemulation/CardEmulation.java @@ -303,12 +303,13 @@ public final class CardEmulation { } /** - * Registers a group of AIDs for the specified service. + * Registers a list of AIDs for a specific category for the + * specified service. * - * <p>If an AID group for that category was previously + * <p>If a list of AIDs 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. + * that list of AIDs 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 the caller of this API. Typically @@ -317,10 +318,13 @@ public final class CardEmulation { * be shared between packages using shared UIDs. * * @param service The component name of the service - * @param aidGroup The group of AIDs to be registered + * @param category The category of AIDs to be registered + * @param aids A list containing the AIDs to be registered * @return whether the registration was successful. */ - public boolean registerAidGroupForService(ComponentName service, AidGroup aidGroup) { + public boolean registerAidsForService(ComponentName service, String category, + List<String> aids) { + AidGroup aidGroup = new AidGroup(aids, category); try { return sService.registerAidGroupForService(UserHandle.myUserId(), service, aidGroup); } catch (RemoteException e) { @@ -341,21 +345,24 @@ public final class CardEmulation { } /** - * Retrieves the currently registered AID group for the specified + * Retrieves the currently registered AIDs 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 + * <p>Note that this will only return AIDs that were dynamically + * registered using {@link #registerAidsForService(ComponentName, String, List)} + * method. It will *not* return AIDs 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 + * @param category The category for which the AIDs were registered, + * e.g. {@link #CATEGORY_PAYMENT} + * @return The list of AIDs registered for this category, or null if it couldn't be found. */ - public AidGroup getAidGroupForService(ComponentName service, String category) { + public List<String> getAidsForService(ComponentName service, String category) { try { - return sService.getAidGroupForService(UserHandle.myUserId(), service, category); + AidGroup group = sService.getAidGroupForService(UserHandle.myUserId(), service, + category); + return (group != null ? group.getAids() : null); } catch (RemoteException e) { recoverService(); if (sService == null) { @@ -363,7 +370,9 @@ public final class CardEmulation { return null; } try { - return sService.getAidGroupForService(UserHandle.myUserId(), service, category); + AidGroup group = sService.getAidGroupForService(UserHandle.myUserId(), service, + category); + return (group != null ? group.getAids() : null); } catch (RemoteException ee) { Log.e(TAG, "Failed to recover CardEmulationService."); return null; @@ -372,21 +381,21 @@ public final class CardEmulation { } /** - * Removes a registered AID group for the specified category for the + * Removes a previously registered list of AIDs 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 + * <p>Note that this will only remove AIDs that were dynamically + * registered using the {@link #registerAidsForService(ComponentName, String, List)} + * method. It will *not* remove AIDs that were statically registered in + * the manifest. If dynamically registered AIDs are removed using * this method, and a statically registered AID group for the same category * exists in the manifest, the static 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} + * @param category The category of the AIDs to be removed, e.g. {@link #CATEGORY_PAYMENT} * @return whether the group was successfully removed. */ - public boolean removeAidGroupForService(ComponentName service, String category) { + public boolean removeAidsForService(ComponentName service, String category) { try { return sService.removeAidGroupForService(UserHandle.myUserId(), service, category); } catch (RemoteException e) { diff --git a/core/java/android/os/CommonBundle.java b/core/java/android/os/BaseBundle.java index e11f170..c2a45ba 100644 --- a/core/java/android/os/CommonBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -18,17 +18,16 @@ 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.Map; import java.util.Set; /** * A mapping from String values to various types. */ -abstract class CommonBundle implements Parcelable, Cloneable { +public class BaseBundle { private static final String TAG = "Bundle"; static final boolean DEBUG = false; @@ -64,7 +63,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * inside of the Bundle. * @param capacity Initial size of the ArrayMap. */ - CommonBundle(ClassLoader loader, int capacity) { + BaseBundle(ClassLoader loader, int capacity) { mMap = capacity > 0 ? new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>(); mClassLoader = loader == null ? getClass().getClassLoader() : loader; @@ -73,7 +72,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { /** * Constructs a new, empty Bundle. */ - CommonBundle() { + BaseBundle() { this((ClassLoader) null, 0); } @@ -83,11 +82,11 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @param parcelledData a Parcel containing a Bundle */ - CommonBundle(Parcel parcelledData) { + BaseBundle(Parcel parcelledData) { readFromParcelInner(parcelledData); } - CommonBundle(Parcel parcelledData, int length) { + BaseBundle(Parcel parcelledData, int length) { readFromParcelInner(parcelledData, length); } @@ -98,7 +97,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param loader An explicit ClassLoader to use when instantiating objects * inside of the Bundle. */ - CommonBundle(ClassLoader loader) { + BaseBundle(ClassLoader loader) { this(loader, 0); } @@ -108,7 +107,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @param capacity the initial capacity of the Bundle */ - CommonBundle(int capacity) { + BaseBundle(int capacity) { this((ClassLoader) null, capacity); } @@ -118,7 +117,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @param b a Bundle to be copied. */ - CommonBundle(CommonBundle b) { + BaseBundle(BaseBundle b) { if (b.mParcelledData != null) { if (b.mParcelledData == EMPTY_PARCEL) { mParcelledData = EMPTY_PARCEL; @@ -149,7 +148,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @hide */ - String getPairValue() { + public String getPairValue() { unparcel(); int size = mMap.size(); if (size > 1) { @@ -229,7 +228,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { /** * @hide */ - boolean isParcelled() { + public boolean isParcelled() { return mParcelledData != null; } @@ -238,7 +237,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @return the number of mappings as an int. */ - int size() { + public int size() { unparcel(); return mMap.size(); } @@ -246,7 +245,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { /** * Returns true if the mapping of this Bundle is empty, false otherwise. */ - boolean isEmpty() { + public boolean isEmpty() { unparcel(); return mMap.isEmpty(); } @@ -254,7 +253,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { /** * Removes all elements from the mapping of this Bundle. */ - void clear() { + public void clear() { unparcel(); mMap.clear(); } @@ -266,7 +265,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String key * @return true if the key is part of the mapping, false otherwise */ - boolean containsKey(String key) { + public boolean containsKey(String key) { unparcel(); return mMap.containsKey(key); } @@ -277,7 +276,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String key * @return an Object, or null */ - Object get(String key) { + public Object get(String key) { unparcel(); return mMap.get(key); } @@ -287,28 +286,38 @@ abstract class CommonBundle implements Parcelable, Cloneable { * * @param key a String key */ - void remove(String key) { + public void remove(String key) { unparcel(); mMap.remove(key); } /** - * Inserts all mappings from the given PersistableBundle into this CommonBundle. + * Inserts all mappings from the given PersistableBundle into this BaseBundle. * * @param bundle a PersistableBundle */ - void putAll(PersistableBundle bundle) { + public void putAll(PersistableBundle bundle) { unparcel(); bundle.unparcel(); mMap.putAll(bundle.mMap); } /** + * Inserts all mappings from the given Map into this BaseBundle. + * + * @param map a Map + */ + void putAll(Map map) { + unparcel(); + mMap.putAll(map); + } + + /** * Returns a Set containing the Strings used as keys in this Bundle. * * @return a Set of String keys */ - Set<String> keySet() { + public Set<String> keySet() { unparcel(); return mMap.keySet(); } @@ -368,7 +377,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an int, or null */ - void putInt(String key, int value) { + public void putInt(String key, int value) { unparcel(); mMap.put(key, value); } @@ -380,7 +389,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a long */ - void putLong(String key, long value) { + public void putLong(String key, long value) { unparcel(); mMap.put(key, value); } @@ -404,7 +413,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a double */ - void putDouble(String key, double value) { + public void putDouble(String key, double value) { unparcel(); mMap.put(key, value); } @@ -416,7 +425,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a String, or null */ - void putString(String key, String value) { + public void putString(String key, String value) { unparcel(); mMap.put(key, value); } @@ -536,7 +545,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value an int array object, or null */ - void putIntArray(String key, int[] value) { + public void putIntArray(String key, int[] value) { unparcel(); mMap.put(key, value); } @@ -548,7 +557,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a long array object, or null */ - void putLongArray(String key, long[] value) { + public void putLongArray(String key, long[] value) { unparcel(); mMap.put(key, value); } @@ -572,7 +581,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a double array object, or null */ - void putDoubleArray(String key, double[] value) { + public void putDoubleArray(String key, double[] value) { unparcel(); mMap.put(key, value); } @@ -584,7 +593,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @param value a String array object, or null */ - void putStringArray(String key, String[] value) { + public void putStringArray(String key, String[] value) { unparcel(); mMap.put(key, value); } @@ -602,18 +611,6 @@ abstract class CommonBundle 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 - */ - 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. * @@ -780,7 +777,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String * @return an int value */ - int getInt(String key) { + public int getInt(String key) { unparcel(); return getInt(key, 0); } @@ -793,7 +790,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return an int value */ - int getInt(String key, int defaultValue) { + public int getInt(String key, int defaultValue) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -814,7 +811,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String * @return a long value */ - long getLong(String key) { + public long getLong(String key) { unparcel(); return getLong(key, 0L); } @@ -827,7 +824,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a long value */ - long getLong(String key, long defaultValue) { + public long getLong(String key, long defaultValue) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -882,7 +879,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String * @return a double value */ - double getDouble(String key) { + public double getDouble(String key) { unparcel(); return getDouble(key, 0.0); } @@ -895,7 +892,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param defaultValue Value to return if key does not exist * @return a double value */ - double getDouble(String key, double defaultValue) { + public double getDouble(String key, double defaultValue) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -917,7 +914,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @return a String value, or null */ - String getString(String key) { + public String getString(String key) { unparcel(); final Object o = mMap.get(key); try { @@ -937,7 +934,7 @@ abstract class CommonBundle 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. */ - String getString(String key, String defaultValue) { + public String getString(String key, String defaultValue) { final String s = getString(key); return (s == null) ? defaultValue : s; } @@ -981,28 +978,6 @@ abstract class CommonBundle implements Parcelable, Cloneable { * 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) { @@ -1181,7 +1156,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @return an int[] value, or null */ - int[] getIntArray(String key) { + public int[] getIntArray(String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1203,7 +1178,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @return a long[] value, or null */ - long[] getLongArray(String key) { + public long[] getLongArray(String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1247,7 +1222,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @return a double[] value, or null */ - double[] getDoubleArray(String key) { + public double[] getDoubleArray(String key) { unparcel(); Object o = mMap.get(key); if (o == null) { @@ -1269,7 +1244,7 @@ abstract class CommonBundle implements Parcelable, Cloneable { * @param key a String, or null * @return a String[] value, or null */ - String[] getStringArray(String key) { + public String[] getStringArray(String key) { unparcel(); Object o = mMap.get(key); if (o == null) { diff --git a/core/java/android/os/BatteryManager.java b/core/java/android/os/BatteryManager.java index f339e52..32050dc 100644 --- a/core/java/android/os/BatteryManager.java +++ b/core/java/android/os/BatteryManager.java @@ -146,9 +146,9 @@ public class BatteryManager { return null; } - BatteryProperty prop = new BatteryProperty(Integer.MIN_VALUE); + BatteryProperty prop = new BatteryProperty(); if ((mBatteryPropertiesRegistrar.getProperty(id, prop) == 0) && - (prop.getInt() != Integer.MIN_VALUE)) + (prop.getLong() != Long.MIN_VALUE)) return prop; else return null; diff --git a/core/java/android/os/BatteryProperty.java b/core/java/android/os/BatteryProperty.java index ec73952..27dad4f 100644 --- a/core/java/android/os/BatteryProperty.java +++ b/core/java/android/os/BatteryProperty.java @@ -53,20 +53,18 @@ public class BatteryProperty implements Parcelable { */ public static final int CAPACITY = 4; - private int mValueInt; - /** - * @hide + * Battery remaining energy in nanowatt-hours, as a long integer. */ - public BatteryProperty(int value) { - mValueInt = value; - } + public static final int ENERGY_COUNTER = 5; + + private long mValueLong; /** * @hide */ public BatteryProperty() { - mValueInt = Integer.MIN_VALUE; + mValueLong = Long.MIN_VALUE; } /** @@ -79,9 +77,21 @@ public class BatteryProperty implements Parcelable { * @return The queried property value, or Integer.MIN_VALUE if not supported. */ public int getInt() { - return mValueInt; + return (int)mValueLong; } + /** + * Return the value of a property of long type previously queried + * via {@link BatteryManager#getProperty + * BatteryManager.getProperty()}. If the platform does + * not provide the property queried, this value will be + * Long.MIN_VALUE. + * + * @return The queried property value, or Long.MIN_VALUE if not supported. + */ + public long getLong() { + return mValueLong; + } /* * Parcel read/write code must be kept in sync with * frameworks/native/services/batteryservice/BatteryProperty.cpp @@ -92,11 +102,11 @@ public class BatteryProperty implements Parcelable { } public void readFromParcel(Parcel p) { - mValueInt = p.readInt(); + mValueLong = p.readLong(); } public void writeToParcel(Parcel p, int flags) { - p.writeInt(mValueInt); + p.writeLong(mValueLong); } public static final Parcelable.Creator<BatteryProperty> CREATOR diff --git a/core/java/android/os/BatteryStats.java b/core/java/android/os/BatteryStats.java index 8b7467f..e627d49 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Formatter; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -30,6 +31,7 @@ import android.telephony.SignalStrength; import android.text.format.DateFormat; import android.util.Printer; import android.util.SparseArray; +import android.util.SparseIntArray; import android.util.TimeUtils; import com.android.internal.os.BatterySipper; import com.android.internal.os.BatteryStatsHelper; @@ -529,7 +531,8 @@ public abstract class BatteryStats implements Parcelable { public final static class HistoryItem implements Parcelable { public HistoryItem next; - + + // The time of this event in milliseconds, as per SystemClock.elapsedRealtime(). public long time; public static final byte CMD_UPDATE = 0; // These can be written as deltas @@ -537,6 +540,7 @@ public abstract class BatteryStats implements Parcelable { public static final byte CMD_START = 4; public static final byte CMD_CURRENT_TIME = 5; public static final byte CMD_OVERFLOW = 6; + public static final byte CMD_RESET = 7; public byte cmd = CMD_NULL; @@ -597,6 +601,7 @@ public abstract class BatteryStats implements Parcelable { public int states; public static final int STATE2_VIDEO_ON_FLAG = 1<<0; + public static final int STATE2_LOW_POWER_FLAG = 1<<1; public int states2; // The wake lock that was acquired at this point. @@ -618,8 +623,13 @@ public abstract class BatteryStats implements Parcelable { 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; + // Events for all additional wake locks aquired/release within a wake block. + // These are not generated by default. + public static final int EVENT_WAKE_LOCK = 0x0005; // Number of event types. - public static final int EVENT_COUNT = 0x0005; + public static final int EVENT_COUNT = 0x0006; + // Mask to extract out only the type part of the event. + public static final int EVENT_TYPE_MASK = ~(EVENT_FLAG_START|EVENT_FLAG_FINISH); public static final int EVENT_PROC_START = EVENT_PROC | EVENT_FLAG_START; public static final int EVENT_PROC_FINISH = EVENT_PROC | EVENT_FLAG_FINISH; @@ -629,12 +639,14 @@ public abstract class BatteryStats implements Parcelable { 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; + public static final int EVENT_WAKE_LOCK_START = EVENT_WAKE_LOCK | EVENT_FLAG_START; + public static final int EVENT_WAKE_LOCK_FINISH = EVENT_WAKE_LOCK | EVENT_FLAG_FINISH; // For CMD_EVENT. public int eventCode; public HistoryTag eventTag; - // Only set for CMD_CURRENT_TIME. + // Only set for CMD_CURRENT_TIME or CMD_RESET, as per System.currentTimeMillis(). public long currentTime; // Meta-data when reading. @@ -684,7 +696,7 @@ public abstract class BatteryStats implements Parcelable { dest.writeInt(eventCode); eventTag.writeToParcel(dest, flags); } - if (cmd == CMD_CURRENT_TIME) { + if (cmd == CMD_CURRENT_TIME || cmd == CMD_RESET) { dest.writeLong(currentTime); } } @@ -722,7 +734,7 @@ public abstract class BatteryStats implements Parcelable { eventCode = EVENT_NONE; eventTag = null; } - if (cmd == CMD_CURRENT_TIME) { + if (cmd == CMD_CURRENT_TIME || cmd == CMD_RESET) { currentTime = src.readLong(); } else { currentTime = 0; @@ -833,7 +845,64 @@ public abstract class BatteryStats implements Parcelable { return true; } } - + + public final static class HistoryEventTracker { + private final HashMap<String, SparseIntArray>[] mActiveEvents + = (HashMap<String, SparseIntArray>[]) new HashMap[HistoryItem.EVENT_COUNT]; + + public boolean updateState(int code, String name, int uid, int poolIdx) { + if ((code&HistoryItem.EVENT_FLAG_START) != 0) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + HashMap<String, SparseIntArray> active = mActiveEvents[idx]; + if (active == null) { + active = new HashMap<String, SparseIntArray>(); + mActiveEvents[idx] = active; + } + SparseIntArray uids = active.get(name); + if (uids == null) { + uids = new SparseIntArray(); + active.put(name, uids); + } + if (uids.indexOfKey(uid) >= 0) { + // Already set, nothing to do! + return false; + } + uids.put(uid, poolIdx); + } else if ((code&HistoryItem.EVENT_FLAG_FINISH) != 0) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + HashMap<String, SparseIntArray> active = mActiveEvents[idx]; + if (active == null) { + // not currently active, nothing to do. + return false; + } + SparseIntArray uids = active.get(name); + if (uids == null) { + // not currently active, nothing to do. + return false; + } + idx = uids.indexOfKey(uid); + if (idx < 0) { + // not currently active, nothing to do. + return false; + } + uids.removeAt(idx); + if (uids.size() <= 0) { + active.remove(name); + } + } + return true; + } + + public void removeEvents(int code) { + int idx = code&HistoryItem.EVENT_TYPE_MASK; + mActiveEvents[idx] = null; + } + + public HashMap<String, SparseIntArray> getStateForEvent(int code) { + return mActiveEvents[code]; + } + } + public static final class BitDescription { public final int mask; public final int shift; @@ -861,7 +930,15 @@ public abstract class BatteryStats implements Parcelable { this.shortValues = shortValues; } } - + + /** + * Don't allow any more batching in to the current history event. This + * is called when printing partial histories, so to ensure that the next + * history event will go in to a new batch after what was printed in the + * last partial history. + */ + public abstract void commitCurrentHistoryBatchLocked(); + public abstract int getHistoryTotalSize(); public abstract int getHistoryUsedSize(); @@ -939,6 +1016,21 @@ public abstract class BatteryStats implements Parcelable { long elapsedRealtimeUs, int which); /** + * Returns the time in microseconds that low power mode has been enabled while the device was + * running on battery. + * + * {@hide} + */ + public abstract long getLowPowerModeEnabledTime(long elapsedRealtimeUs, int which); + + /** + * Returns the number of times that low power mode was enabled. + * + * {@hide} + */ + public abstract int getLowPowerModeEnabledCount(int which); + + /** * Returns the time in microseconds that the phone has been on while the device was * running on battery. * @@ -1099,14 +1191,15 @@ public abstract class BatteryStats implements Parcelable { public static final BitDescription[] HISTORY_STATE2_DESCRIPTIONS = new BitDescription[] { new BitDescription(HistoryItem.STATE2_VIDEO_ON_FLAG, "video", "v"), + new BitDescription(HistoryItem.STATE2_LOW_POWER_FLAG, "low_power", "lp"), }; public static final String[] HISTORY_EVENT_NAMES = new String[] { - "null", "proc", "fg", "top", "sync" + "null", "proc", "fg", "top", "sync", "wake_lock_in" }; public static final String[] HISTORY_EVENT_CHECKIN_NAMES = new String[] { - "Enl", "Epr", "Efg", "Etp", "Esy" + "Enl", "Epr", "Efg", "Etp", "Esy", "Ewl" }; /** @@ -1572,11 +1665,12 @@ public abstract class BatteryStats implements Parcelable { final long totalUptime = computeUptime(rawUptime, which); final long screenOnTime = getScreenOnTime(rawRealtime, which); final long interactiveTime = getInteractiveTime(rawRealtime, which); + final long lowPowerModeEnabledTime = getLowPowerModeEnabledTime(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); SparseArray<? extends Uid> uidStats = getUidStats(); @@ -1641,7 +1735,8 @@ public abstract class BatteryStats implements Parcelable { mobileRxTotalBytes, mobileTxTotalBytes, wifiRxTotalBytes, wifiTxTotalBytes, fullWakeLockTimeTotal, partialWakeLockTimeTotal, 0 /*legacy input event count*/, getMobileRadioActiveTime(rawRealtime, which), - getMobileRadioActiveAdjustedTime(which), interactiveTime / 1000); + getMobileRadioActiveAdjustedTime(which), interactiveTime / 1000, + lowPowerModeEnabledTime / 1000); // Dump screen brightness stats Object[] args = new Object[NUM_SCREEN_BRIGHTNESS_BINS]; @@ -2034,32 +2129,20 @@ public abstract class BatteryStats implements Parcelable { final long screenOnTime = getScreenOnTime(rawRealtime, which); final long interactiveTime = getInteractiveTime(rawRealtime, which); + final long lowPowerModeEnabledTime = getLowPowerModeEnabledTime(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(" Interactive: "); formatTimeMs(sb, interactiveTime / 1000); - sb.append("("); sb.append(formatRatioLocked(interactiveTime, whichBatteryRealtime)); - sb.append(")"); - pw.println(sb.toString()); - sb.setLength(0); - sb.append(prefix); sb.append(" Screen on: "); formatTimeMs(sb, screenOnTime / 1000); sb.append("("); sb.append(formatRatioLocked(screenOnTime, whichBatteryRealtime)); sb.append(") "); sb.append(getScreenOnCount(which)); - sb.append("x, Active phone call: "); formatTimeMs(sb, phoneOnTime / 1000); - sb.append("("); sb.append(formatRatioLocked(phoneOnTime, whichBatteryRealtime)); + sb.append("x, Interactive: "); formatTimeMs(sb, interactiveTime / 1000); + sb.append("("); sb.append(formatRatioLocked(interactiveTime, whichBatteryRealtime)); sb.append(")"); 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:"); @@ -2081,7 +2164,24 @@ public abstract class BatteryStats implements Parcelable { } if (!didOne) sb.append(" (no activity)"); pw.println(sb.toString()); - + if (lowPowerModeEnabledTime != 0) { + sb.setLength(0); + sb.append(prefix); + sb.append(" Low power mode enabled: "); + formatTimeMs(sb, lowPowerModeEnabledTime / 1000); + sb.append("("); + sb.append(formatRatioLocked(lowPowerModeEnabledTime, whichBatteryRealtime)); + sb.append(")"); + 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)); + } + // Calculate wakelock times across all uids. long fullWakeLockTimeTotalMicros = 0; long partialWakeLockTimeTotalMicros = 0; @@ -2351,8 +2451,10 @@ public abstract class BatteryStats implements Parcelable { 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.print(", actual drain: "); printmAh(pw, helper.getMinDrainedPower()); + if (helper.getMinDrainedPower() != helper.getMaxDrainedPower()) { + pw.print("-"); printmAh(pw, helper.getMaxDrainedPower()); + } pw.println(); for (int i=0; i<sippers.size(); i++) { BatterySipper bs = sippers.get(i); @@ -2946,6 +3048,8 @@ public abstract class BatteryStats implements Parcelable { pw.print(rec.numReadInts); pw.print(") "); } else { + pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); + pw.print(HISTORY_DATA); pw.print(','); if (lastTime < 0) { pw.print(rec.time - baseTime); } else { @@ -2958,10 +3062,14 @@ public abstract class BatteryStats implements Parcelable { pw.print(":"); } pw.println("START"); - } else if (rec.cmd == HistoryItem.CMD_CURRENT_TIME) { + } else if (rec.cmd == HistoryItem.CMD_CURRENT_TIME + || rec.cmd == HistoryItem.CMD_RESET) { if (checkin) { pw.print(":"); } + if (rec.cmd == HistoryItem.CMD_RESET) { + pw.print("RESET:"); + } pw.print("TIME:"); if (checkin) { pw.println(rec.currentTime); @@ -3128,6 +3236,7 @@ public abstract class BatteryStats implements Parcelable { } pw.println(); oldState = rec.states; + oldState2 = rec.states2; } } } @@ -3187,6 +3296,89 @@ public abstract class BatteryStats implements Parcelable { public static final int DUMP_INCLUDE_HISTORY = 1<<3; public static final int DUMP_VERBOSE = 1<<4; + private void dumpHistoryLocked(PrintWriter pw, int flags, long histStart, boolean checkin) { + final HistoryPrinter hprinter = new HistoryPrinter(); + final HistoryItem rec = new HistoryItem(); + long lastTime = -1; + long baseTime = -1; + boolean printed = false; + HistoryEventTracker tracker = null; + 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 + || rec.cmd == HistoryItem.CMD_RESET + || rec.cmd == HistoryItem.CMD_START) { + printed = true; + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = HistoryItem.CMD_UPDATE; + } else if (rec.currentTime != 0) { + printed = true; + byte cmd = rec.cmd; + rec.cmd = HistoryItem.CMD_CURRENT_TIME; + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = cmd; + } + if (tracker != null) { + if (rec.cmd != HistoryItem.CMD_UPDATE) { + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.cmd = HistoryItem.CMD_UPDATE; + } + int oldEventCode = rec.eventCode; + HistoryTag oldEventTag = rec.eventTag; + rec.eventTag = new HistoryTag(); + for (int i=0; i<HistoryItem.EVENT_COUNT; i++) { + HashMap<String, SparseIntArray> active + = tracker.getStateForEvent(i); + if (active == null) { + continue; + } + for (HashMap.Entry<String, SparseIntArray> ent + : active.entrySet()) { + SparseIntArray uids = ent.getValue(); + for (int j=0; j<uids.size(); j++) { + rec.eventCode = i; + rec.eventTag.string = ent.getKey(); + rec.eventTag.uid = uids.keyAt(j); + rec.eventTag.poolIdx = uids.valueAt(j); + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + rec.wakeReasonTag = null; + rec.wakelockTag = null; + } + } + } + rec.eventCode = oldEventCode; + rec.eventTag = oldEventTag; + tracker = null; + } + } + hprinter.printNextItem(pw, rec, baseTime, checkin, + (flags&DUMP_VERBOSE) != 0); + } else if (false && rec.eventCode != HistoryItem.EVENT_NONE) { + // This is an attempt to aggregate the previous state and generate + //Â fake events to reflect that state at the point where we start + // printing real events. It doesn't really work right, so is turned off. + if (tracker == null) { + tracker = new HistoryEventTracker(); + } + tracker.updateState(rec.eventCode, rec.eventTag.string, + rec.eventTag.uid, rec.eventTag.poolIdx); + } + } + if (histStart >= 0) { + commitCurrentHistoryBatchLocked(); + pw.print(checkin ? "NEXT: " : " NEXT: "); pw.println(lastTime+1); + } + } + /** * Dumps a human-readable summary of the battery statistics to the given PrintWriter. * @@ -3200,9 +3392,6 @@ public abstract class BatteryStats implements Parcelable { (flags&(DUMP_HISTORY_ONLY|DUMP_UNPLUGGED_ONLY|DUMP_CHARGED_ONLY)) != 0; if ((flags&DUMP_HISTORY_ONLY) != 0 || !filtering) { - long now = getHistoryBaseTime() + SystemClock.elapsedRealtime(); - - final HistoryItem rec = new HistoryItem(); final long historyTotalSize = getHistoryTotalSize(); final long historyUsedSize = getHistoryUsedSize(); if (startIteratingHistoryLocked()) { @@ -3218,35 +3407,7 @@ public abstract class BatteryStats implements Parcelable { 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); - } + dumpHistoryLocked(pw, flags, histStart, false); pw.println(); } finally { finishIteratingHistoryLocked(); @@ -3255,6 +3416,7 @@ public abstract class BatteryStats implements Parcelable { if (startIteratingOldHistoryLocked()) { try { + final HistoryItem rec = new HistoryItem(); pw.println("Old battery History:"); HistoryPrinter hprinter = new HistoryPrinter(); long baseTime = -1; @@ -3348,7 +3510,6 @@ public abstract class BatteryStats implements Parcelable { (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()) { try { for (int i=0; i<getHistoryStringPoolSize(); i++) { @@ -3365,37 +3526,7 @@ public abstract class BatteryStats implements Parcelable { 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); - } + dumpHistoryLocked(pw, flags, histStart, true); } finally { finishIteratingHistoryLocked(); } diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 1ca6b90..a7485b4 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -88,7 +88,8 @@ public class Build { * * @hide */ - public static final String[] SUPPORTED_ABIS = getString("ro.product.cpu.abilist").split(","); + public static final String[] SUPPORTED_ABIS = SystemProperties.get("ro.product.cpu.abilist") + .split(","); /** * An ordered list of <b>32 bit</b> ABIs supported by this device. The most preferred ABI @@ -98,8 +99,8 @@ public class Build { * * @hide */ - public static final String[] SUPPORTED_32_BIT_ABIS = getString("ro.product.cpu.abilist32") - .split(","); + public static final String[] SUPPORTED_32_BIT_ABIS = + SystemProperties.get("ro.product.cpu.abilist32").split(","); /** * An ordered list of <b>64 bit</b> ABIs supported by this device. The most preferred ABI @@ -109,8 +110,8 @@ public class Build { * * @hide */ - public static final String[] SUPPORTED_64_BIT_ABIS = getString("ro.product.cpu.abilist64") - .split(","); + public static final String[] SUPPORTED_64_BIT_ABIS = + SystemProperties.get("ro.product.cpu.abilist64").split(","); /** Various version strings. */ @@ -515,9 +516,16 @@ public class Build { public static final int KITKAT = 19; /** - * Android 4.5: KitKat for watches, snacks on the run. + * Android 4.4W: KitKat for watches, snacks on the run. + * + * <p>Applications targeting this or a later release will get these + * new changes in behavior:</p> + * <ul> + * <li>{@link android.app.AlertDialog} might not have a default background if the theme does + * not specify one.</li> + * </ul> */ - public static final int KITKAT_WATCH = CUR_DEVELOPMENT; // STOPSHIP: update API level + public static final int KITKAT_WATCH = 20; /** * L! diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index c85e418..e42c3fe 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -28,14 +28,14 @@ import java.util.Set; * A mapping from String values to various Parcelable types. * */ -public final class Bundle extends CommonBundle { +public final class Bundle extends BaseBundle implements Cloneable, Parcelable { public static final Bundle EMPTY; static final Parcel EMPTY_PARCEL; static { EMPTY = new Bundle(); EMPTY.mMap = ArrayMap.EMPTY; - EMPTY_PARCEL = CommonBundle.EMPTY_PARCEL; + EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL; } private boolean mHasFds = false; @@ -125,14 +125,6 @@ public final class Bundle extends CommonBundle { } /** - * @hide - */ - @Override - public String getPairValue() { - return super.getPairValue(); - } - - /** * Changes the ClassLoader this Bundle uses when instantiating objects. * * @param loader An explicit ClassLoader to use when instantiating objects @@ -168,32 +160,6 @@ public final class Bundle extends CommonBundle { } /** - * @hide - */ - @Override - public boolean isParcelled() { - return super.isParcelled(); - } - - /** - * Returns the number of mappings contained in this Bundle. - * - * @return the number of mappings as an int. - */ - @Override - public int size() { - return super.size(); - } - - /** - * Returns true if the mapping of this Bundle is empty, false otherwise. - */ - @Override - public boolean isEmpty() { - return super.isEmpty(); - } - - /** * Removes all elements from the mapping of this Bundle. */ @Override @@ -205,39 +171,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 - */ - @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 Bundle. - * - * @param key a String key - */ - @Override - public void remove(String key) { - super.remove(key); - } - - /** * Inserts all mappings from the given Bundle into this Bundle. * * @param bundle a Bundle @@ -253,25 +186,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 Bundle. - * - * @return a Set of String keys - */ - @Override - public Set<String> keySet() { - return super.keySet(); - } - - /** * Reports whether the bundle contains any parcelled file descriptors. */ public boolean hasFileDescriptors() { @@ -384,30 +298,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 - */ - @Override - public void putInt(String key, int value) { - super.putInt(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 - */ - @Override - public void putLong(String key, long value) { - super.putLong(key, value); - } - - /** * Inserts a float value into the mapping of this Bundle, replacing * any existing value for the given key. * @@ -420,30 +310,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 - */ - @Override - public void putDouble(String key, double value) { - super.putDouble(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 - */ - @Override - public void putString(String key, String value) { - super.putString(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. * @@ -616,30 +482,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 - */ - @Override - public void putIntArray(String key, int[] value) { - super.putIntArray(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 - */ - @Override - public void putLongArray(String key, long[] value) { - super.putLongArray(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. * @@ -652,30 +494,6 @@ public final class Bundle extends CommonBundle { } /** - * 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 - */ - @Override - public void putDoubleArray(String key, double[] value) { - super.putDoubleArray(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 - */ - @Override - public void putStringArray(String key, String[] value) { - super.putStringArray(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. * @@ -700,17 +518,6 @@ public final class Bundle extends CommonBundle { } /** - * 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. * @@ -846,56 +653,6 @@ public final class Bundle extends CommonBundle { } /** - * 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.0f if * no mapping of the desired type exists for the given key. * @@ -921,58 +678,6 @@ public final class Bundle extends CommonBundle { } /** - * 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. @@ -1027,18 +732,6 @@ public final class Bundle extends CommonBundle { * 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) { @@ -1232,32 +925,6 @@ public final class Bundle extends CommonBundle { * 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 float[] value, or null */ @Override @@ -1271,32 +938,6 @@ public final class Bundle extends CommonBundle { * 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); - } - - /** - * 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 */ @Override diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index e98a26b..e84b695 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -191,6 +191,10 @@ public class Environment { return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_MEDIA, packageName); } + public File[] buildExternalStorageAppMediaDirsForVold(String packageName) { + return buildPaths(mExternalDirsForVold, DIR_ANDROID, DIR_MEDIA, packageName); + } + public File[] buildExternalStorageAppObbDirs(String packageName) { return buildPaths(mExternalDirsForApp, DIR_ANDROID, DIR_OBB, packageName); } diff --git a/core/java/android/os/FileBridge.java b/core/java/android/os/FileBridge.java new file mode 100644 index 0000000..7f8bc9f --- /dev/null +++ b/core/java/android/os/FileBridge.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import static android.system.OsConstants.AF_UNIX; +import static android.system.OsConstants.SOCK_STREAM; + +import android.system.ErrnoException; +import android.system.Os; +import android.util.Log; + +import libcore.io.IoBridge; +import libcore.io.IoUtils; +import libcore.io.Memory; +import libcore.io.Streams; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.OutputStream; +import java.io.SyncFailedException; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * Simple bridge that allows file access across process boundaries without + * returning the underlying {@link FileDescriptor}. This is useful when the + * server side needs to strongly assert that a client side is completely + * hands-off. + * + * @hide + */ +public class FileBridge extends Thread { + private static final String TAG = "FileBridge"; + + // TODO: consider extending to support bidirectional IO + + private static final int MSG_LENGTH = 8; + + /** CMD_WRITE [len] [data] */ + private static final int CMD_WRITE = 1; + /** CMD_FSYNC */ + private static final int CMD_FSYNC = 2; + + private FileDescriptor mTarget; + + private final FileDescriptor mServer = new FileDescriptor(); + private final FileDescriptor mClient = new FileDescriptor(); + + private volatile boolean mClosed; + + public FileBridge() { + try { + Os.socketpair(AF_UNIX, SOCK_STREAM, 0, mServer, mClient); + } catch (ErrnoException e) { + throw new RuntimeException("Failed to create bridge"); + } + } + + public boolean isClosed() { + return mClosed; + } + + public void setTargetFile(FileDescriptor target) { + mTarget = target; + } + + public FileDescriptor getClientSocket() { + return mClient; + } + + @Override + public void run() { + final byte[] temp = new byte[8192]; + try { + while (IoBridge.read(mServer, temp, 0, MSG_LENGTH) == MSG_LENGTH) { + final int cmd = Memory.peekInt(temp, 0, ByteOrder.BIG_ENDIAN); + + if (cmd == CMD_WRITE) { + // Shuttle data into local file + int len = Memory.peekInt(temp, 4, ByteOrder.BIG_ENDIAN); + while (len > 0) { + int n = IoBridge.read(mServer, temp, 0, Math.min(temp.length, len)); + IoBridge.write(mTarget, temp, 0, n); + len -= n; + } + + } else if (cmd == CMD_FSYNC) { + // Sync and echo back to confirm + Os.fsync(mTarget); + IoBridge.write(mServer, temp, 0, MSG_LENGTH); + } + } + + // Client was closed; one last fsync + Os.fsync(mTarget); + + } catch (ErrnoException e) { + Log.e(TAG, "Failed during bridge: ", e); + } catch (IOException e) { + Log.e(TAG, "Failed during bridge: ", e); + } finally { + IoUtils.closeQuietly(mTarget); + IoUtils.closeQuietly(mServer); + IoUtils.closeQuietly(mClient); + mClosed = true; + } + } + + public static class FileBridgeOutputStream extends OutputStream { + private final FileDescriptor mClient; + private final byte[] mTemp = new byte[MSG_LENGTH]; + + public FileBridgeOutputStream(FileDescriptor client) { + mClient = client; + } + + @Override + public void close() throws IOException { + IoBridge.closeAndSignalBlockedThreads(mClient); + } + + @Override + public void flush() throws IOException { + Memory.pokeInt(mTemp, 0, CMD_FSYNC, ByteOrder.BIG_ENDIAN); + IoBridge.write(mClient, mTemp, 0, MSG_LENGTH); + + // Wait for server to ack + if (IoBridge.read(mClient, mTemp, 0, MSG_LENGTH) == MSG_LENGTH) { + if (Memory.peekInt(mTemp, 0, ByteOrder.BIG_ENDIAN) == CMD_FSYNC) { + return; + } + } + + throw new SyncFailedException("Failed to fsync() across bridge"); + } + + @Override + public void write(byte[] buffer, int byteOffset, int byteCount) throws IOException { + Arrays.checkOffsetAndCount(buffer.length, byteOffset, byteCount); + Memory.pokeInt(mTemp, 0, CMD_WRITE, ByteOrder.BIG_ENDIAN); + Memory.pokeInt(mTemp, 4, byteCount, ByteOrder.BIG_ENDIAN); + IoBridge.write(mClient, mTemp, 0, MSG_LENGTH); + IoBridge.write(mClient, buffer, byteOffset, byteCount); + } + + @Override + public void write(int oneByte) throws IOException { + Streams.writeSingleByte(this, oneByte); + } + } +} diff --git a/core/java/android/os/FileUtils.java b/core/java/android/os/FileUtils.java index d71c3e6..1dba77d 100644 --- a/core/java/android/os/FileUtils.java +++ b/core/java/android/os/FileUtils.java @@ -17,6 +17,7 @@ package android.os; import android.system.ErrnoException; +import android.text.TextUtils; import android.system.Os; import android.system.OsConstants; import android.util.Log; @@ -382,4 +383,32 @@ public class FileUtils { } return filePath.startsWith(dirPath); } + + public static void deleteContents(File dir) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + file.delete(); + } + } + } + + /** + * Assert that given filename is valid on ext4. + */ + public static boolean isValidExtFilename(String name) { + if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) { + return false; + } + for (int i = 0; i < name.length(); i++) { + final char c = name.charAt(i); + if (c == '\0' || c == '/') { + return false; + } + } + return true; + } } diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index f5ff185..eb9ba13 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -99,24 +99,12 @@ interface INetworkManagementService /** * Add the specified route to the interface. */ - void addRoute(String iface, in RouteInfo route); + void addRoute(int netId, in RouteInfo route); /** * Remove the specified route from the interface. */ - void removeRoute(String iface, in RouteInfo route); - - /** - * Add the specified route to a secondary interface - * This will go into a special route table to be accessed - * via ip rules - */ - void addSecondaryRoute(String iface, in RouteInfo route); - - /** - * Remove the specified secondary route. - */ - void removeSecondaryRoute(String iface, in RouteInfo route); + void removeRoute(int netId, in RouteInfo route); /** * Set the specified MTU size @@ -320,24 +308,14 @@ interface INetworkManagementService void removeIdleTimer(String iface); /** - * Sets the name of the default interface in the DNS resolver. - */ - void setDefaultInterfaceForDns(String iface); - - /** - * Bind name servers to an interface in the DNS resolver. - */ - void setDnsServersForInterface(String iface, in String[] servers, String domains); - - /** - * Flush the DNS cache associated with the default interface. + * Bind name servers to a network in the DNS resolver. */ - void flushDefaultDnsCache(); + void setDnsServersForNetwork(int netId, in String[] servers, String domains); /** - * Flush the DNS cache associated with the specified interface. + * Flush the DNS cache associated with the specified network. */ - void flushInterfaceDnsCache(String iface); + void flushNetworkDnsCache(int netId); void setFirewallEnabled(boolean enabled); boolean isFirewallEnabled(); @@ -350,7 +328,7 @@ interface INetworkManagementService * Set all packets from users [uid_start,uid_end] to go through interface iface * iface must already be set for marked forwarding by {@link setMarkedForwarding} */ - void setUidRangeRoute(String iface, int uid_start, int uid_end); + void setUidRangeRoute(String iface, int uid_start, int uid_end, boolean forward_dns); /** * Clears the special routing rules for users [uid_start,uid_end] @@ -402,31 +380,6 @@ interface INetworkManagementService void clearHostExemption(in LinkAddress host); /** - * Set a process (pid) to use the name servers associated with the specified interface. - */ - void setDnsInterfaceForPid(String iface, int pid); - - /** - * Clear a process (pid) from being associated with an interface. - */ - void clearDnsInterfaceForPid(int pid); - - /** - * Set a range of user ids to use the name servers associated with the specified interface. - */ - void setDnsInterfaceForUidRange(String iface, int uid_start, int uid_end); - - /** - * Clear a user range from being associated with an interface. - */ - void clearDnsInterfaceForUidRange(String iface, int uid_start, int uid_end); - - /** - * Clear the mappings from pid to Dns interface and from uid range to Dns interface. - */ - void clearDnsInterfaceMaps(); - - /** * Start the clatd (464xlat) service */ void startClatd(String interfaceName); @@ -455,4 +408,33 @@ interface INetworkManagementService * Check whether the mobile radio is currently active. */ boolean isNetworkActive(); + + /** + * Setup a new network. + */ + void createNetwork(int netId); + + /** + * Remove a network. + */ + void removeNetwork(int netId); + + /** + * Add an interface to a network. + */ + void addInterfaceToNetwork(String iface, int netId); + + /** + * Remove an Interface from a network. + */ + void removeInterfaceFromNetwork(String iface, int netId); + + void addLegacyRouteForNetId(int netId, in RouteInfo routeInfo, int uid); + void removeLegacyRouteForNetId(int netId, in RouteInfo routeInfo, int uid); + + void setDefaultNetId(int netId); + void clearDefaultNetId(); + + void setPermission(boolean internal, boolean changeNetState, in int[] uids); + void clearPermission(in int[] uids); } diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 6c7b08d..61194e9 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -38,7 +38,7 @@ interface IPowerManager void userActivity(long time, int event, int flags); void wakeUp(long time); - void goToSleep(long time, int reason); + void goToSleep(long time, int reason, int flags); void nap(long time); boolean isInteractive(); diff --git a/core/java/android/os/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index c2cd3be..c01f688 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -17,7 +17,14 @@ package android.os; import android.util.ArrayMap; - +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; import java.util.Set; /** @@ -25,14 +32,16 @@ import java.util.Set; * restored. * */ -public final class PersistableBundle extends CommonBundle { +public final class PersistableBundle extends BaseBundle implements Cloneable, Parcelable, + XmlUtils.WriteMapCallback { + private static final String TAG_PERSISTABLEMAP = "pbundle_as_map"; public static final PersistableBundle EMPTY; static final Parcel EMPTY_PARCEL; static { EMPTY = new PersistableBundle(); EMPTY.mMap = ArrayMap.EMPTY; - EMPTY_PARCEL = CommonBundle.EMPTY_PARCEL; + EMPTY_PARCEL = BaseBundle.EMPTY_PARCEL; } /** @@ -43,31 +52,6 @@ public final class PersistableBundle extends CommonBundle { } /** - * 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. * @@ -88,41 +72,50 @@ public final class PersistableBundle extends CommonBundle { } /** - * Make a PersistableBundle for a single key/value pair. + * Constructs a PersistableBundle containing the mappings passed in. * - * @hide + * @param map a Map containing only those items that can be persisted. + * @throws IllegalArgumentException if any element of #map cannot be persisted. */ - public static PersistableBundle forPair(String key, String value) { - PersistableBundle b = new PersistableBundle(1); - b.putString(key, value); - return b; - } + private PersistableBundle(Map<String, Object> map) { + super(); - /** - * @hide - */ - @Override - public String getPairValue() { - return super.getPairValue(); + // First stuff everything in. + putAll(map); + + // Now verify each item throwing an exception if there is a violation. + Set<String> keys = map.keySet(); + Iterator<String> iterator = keys.iterator(); + while (iterator.hasNext()) { + String key = iterator.next(); + Object value = map.get(key); + if (value instanceof Map) { + // Fix up any Maps by replacing them with PersistableBundles. + putPersistableBundle(key, new PersistableBundle((Map<String, Object>) value)); + } else if (!(value instanceof Integer) && !(value instanceof Long) && + !(value instanceof Double) && !(value instanceof String) && + !(value instanceof int[]) && !(value instanceof long[]) && + !(value instanceof double[]) && !(value instanceof String[]) && + !(value instanceof PersistableBundle) && (value != null)) { + throw new IllegalArgumentException("Bad value in PersistableBundle key=" + key + + " value=" + value); + } + } } - /** - * 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); + /* package */ PersistableBundle(Parcel parcelledData, int length) { + super(parcelledData, length); } /** - * Return the ClassLoader currently associated with this PersistableBundle. + * Make a PersistableBundle for a single key/value pair. + * + * @hide */ - @Override - public ClassLoader getClassLoader() { - return super.getClassLoader(); + public static PersistableBundle forPair(String key, String value) { + PersistableBundle b = new PersistableBundle(1); + b.putString(key, value); + return b; } /** @@ -135,188 +128,6 @@ public final class PersistableBundle extends CommonBundle { } /** - * @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. * @@ -324,109 +135,8 @@ public final class PersistableBundle extends CommonBundle { * @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); + unparcel(); + mMap.put(key, value); } /** @@ -437,61 +147,18 @@ public final class PersistableBundle extends CommonBundle { * @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); + 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; + } } public static final Parcelable.Creator<PersistableBundle> CREATOR = @@ -507,6 +174,38 @@ public final class PersistableBundle extends CommonBundle { } }; + /** @hide */ + @Override + public void writeUnknownObject(Object v, String name, XmlSerializer out) + throws XmlPullParserException, IOException { + if (v instanceof PersistableBundle) { + out.startTag(null, TAG_PERSISTABLEMAP); + out.attribute(null, "name", name); + ((PersistableBundle) v).saveToXml(out); + out.endTag(null, TAG_PERSISTABLEMAP); + } else { + throw new XmlPullParserException("Unknown Object o=" + v); + } + } + + /** @hide */ + public void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException { + unparcel(); + XmlUtils.writeMapXml(mMap, out, this); + } + + /** @hide */ + static class MyReadMapCallback implements XmlUtils.ReadMapCallback { + @Override + public Object readThisUnknownObjectXml(XmlPullParser in, String tag) + throws XmlPullParserException, IOException { + if (TAG_PERSISTABLEMAP.equals(tag)) { + return restoreFromXml(in); + } + throw new XmlPullParserException("Unknown tag=" + tag); + } + } + /** * Report the nature of this Parcelable's contents */ @@ -524,19 +223,27 @@ public final class PersistableBundle extends CommonBundle { public void writeToParcel(Parcel parcel, int flags) { final boolean oldAllowFds = parcel.pushAllowFds(false); try { - super.writeToParcelInner(parcel, flags); + 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); + /** @hide */ + public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException, + XmlPullParserException { + final int outerDepth = in.getDepth(); + final String startTag = in.getName(); + final String[] tagName = new String[1]; + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + return new PersistableBundle((Map<String, Object>) + XmlUtils.readThisMapXml(in, startTag, tagName, new MyReadMapCallback())); + } + } + return EMPTY; } @Override diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index f8d7c3e..d5177e8 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -321,6 +321,12 @@ public final class PowerManager { */ public static final String REBOOT_RECOVERY = "recovery"; + /** + * Go to sleep flag: Skip dozing state and directly go to full sleep. + * @hide + */ + public static final int GO_TO_SLEEP_FLAG_NO_DOZE = 1 << 0; + final Context mContext; final IPowerManager mService; final Handler mHandler; @@ -366,15 +372,6 @@ public final class PowerManager { } /** - * Returns true if the screen auto-brightness adjustment setting should - * be available in the UI. This setting is experimental and disabled by default. - * @hide - */ - public static boolean useScreenAutoBrightnessAdjustmentFeature() { - return SystemProperties.getBoolean("persist.power.useautobrightadj", false); - } - - /** * Returns true if the twilight service should be used to adjust screen brightness * policy. This setting is experimental and disabled by default. * @hide @@ -509,8 +506,15 @@ public final class PowerManager { * @see #wakeUp */ public void goToSleep(long time) { + goToSleep(time, GO_TO_SLEEP_REASON_USER, 0); + } + + /** + * @hide + */ + public void goToSleep(long time, int reason, int flags) { try { - mService.goToSleep(time, GO_TO_SLEEP_REASON_USER); + mService.goToSleep(time, reason, flags); } catch (RemoteException e) { } } diff --git a/core/java/android/os/PowerManagerInternal.java b/core/java/android/os/PowerManagerInternal.java index cb3d528..69b828f 100644 --- a/core/java/android/os/PowerManagerInternal.java +++ b/core/java/android/os/PowerManagerInternal.java @@ -55,6 +55,14 @@ public abstract class PowerManagerInternal { */ public abstract void setUserActivityTimeoutOverrideFromWindowManager(long timeoutMillis); + public abstract boolean getLowPowerModeEnabled(); + + public interface LowPowerModeListener { + public void onLowPowerModeChanged(boolean enabled); + } + + public abstract void registerLowPowerModeObserver(LowPowerModeListener listener); + // TODO: Remove this and retrieve as a local service instead. public abstract void setPolicy(WindowManagerPolicy policy); } diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 1b3aa0a..112ec1d 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -31,13 +31,17 @@ import java.util.Arrays; import java.util.List; /*package*/ class ZygoteStartFailedEx extends Exception { - /** - * Something prevented the zygote process startup from happening normally - */ + ZygoteStartFailedEx(String s) { + super(s); + } - ZygoteStartFailedEx() {}; - ZygoteStartFailedEx(String s) {super(s);} - ZygoteStartFailedEx(Throwable cause) {super(cause);} + ZygoteStartFailedEx(Throwable cause) { + super(cause); + } + + ZygoteStartFailedEx(String s, Throwable cause) { + super(s, cause); + } } /** @@ -46,9 +50,15 @@ import java.util.List; public class Process { private static final String LOG_TAG = "Process"; - private static final String ZYGOTE_SOCKET = "zygote"; + /** + * @hide for internal use only. + */ + public static final String ZYGOTE_SOCKET = "zygote"; - private static final String SECONDARY_ZYGOTE_SOCKET = "zygote_secondary"; + /** + * @hide for internal use only. + */ + public static final String SECONDARY_ZYGOTE_SOCKET = "zygote_secondary"; /** * Defines the UID/GID under which system code runs. @@ -338,8 +348,10 @@ public class Process { /** * State for communicating with the zygote process. + * + * @hide for internal use only. */ - static class ZygoteState { + public static class ZygoteState { final LocalSocket socket; final DataInputStream inputStream; final BufferedWriter writer; @@ -355,55 +367,26 @@ public class Process { this.abiList = abiList; } - static ZygoteState connect(String socketAddress, int tries) throws ZygoteStartFailedEx { - LocalSocket zygoteSocket = null; + public static ZygoteState connect(String socketAddress) throws IOException { DataInputStream zygoteInputStream = null; BufferedWriter zygoteWriter = null; + final LocalSocket zygoteSocket = new LocalSocket(); - /* - * See bug #811181: Sometimes runtime can make it up before zygote. - * Really, we'd like to do something better to avoid this condition, - * but for now just wait a bit... - * - * TODO: This bug was filed in 2007. Get rid of this code. The zygote - * forks the system_server so it shouldn't be possible for the zygote - * socket to be brought up after the system_server is. - */ - for (int i = 0; i < tries; i++) { - if (i > 0) { - try { - Log.i(LOG_TAG, "Zygote not up yet, sleeping..."); - Thread.sleep(ZYGOTE_RETRY_MILLIS); - } catch (InterruptedException ex) { - throw new ZygoteStartFailedEx(ex); - } - } + try { + zygoteSocket.connect(new LocalSocketAddress(socketAddress, + LocalSocketAddress.Namespace.RESERVED)); - try { - zygoteSocket = new LocalSocket(); - zygoteSocket.connect(new LocalSocketAddress(socketAddress, - LocalSocketAddress.Namespace.RESERVED)); - - zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream()); - - zygoteWriter = new BufferedWriter(new OutputStreamWriter( - zygoteSocket.getOutputStream()), 256); - break; - } catch (IOException ex) { - if (zygoteSocket != null) { - try { - zygoteSocket.close(); - } catch (IOException ex2) { - Log.e(LOG_TAG,"I/O exception on close after exception", ex2); - } - } + zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream()); - zygoteSocket = null; + zygoteWriter = new BufferedWriter(new OutputStreamWriter( + zygoteSocket.getOutputStream()), 256); + } catch (IOException ex) { + try { + zygoteSocket.close(); + } catch (IOException ignore) { } - } - if (zygoteSocket == null) { - throw new ZygoteStartFailedEx("connect failed"); + throw ex; } String abiListString = getAbiList(zygoteWriter, zygoteInputStream); @@ -417,7 +400,7 @@ public class Process { return abiList.contains(abi); } - void close() { + public void close() { try { socket.close(); } catch (IOException ex) { @@ -503,27 +486,22 @@ public class Process { * @throws ZygoteStartFailedEx if the query failed. */ private static String getAbiList(BufferedWriter writer, DataInputStream inputStream) - throws ZygoteStartFailedEx { - try { - - // Each query starts with the argument count (1 in this case) - writer.write("1"); - // ... followed by a new-line. - writer.newLine(); - // ... followed by our only argument. - writer.write("--query-abi-list"); - writer.newLine(); - writer.flush(); - - // The response is a length prefixed stream of ASCII bytes. - int numBytes = inputStream.readInt(); - byte[] bytes = new byte[numBytes]; - inputStream.readFully(bytes); - - return new String(bytes, StandardCharsets.US_ASCII); - } catch (IOException ioe) { - throw new ZygoteStartFailedEx(ioe); - } + throws IOException { + // Each query starts with the argument count (1 in this case) + writer.write("1"); + // ... followed by a new-line. + writer.newLine(); + // ... followed by our only argument. + writer.write("--query-abi-list"); + writer.newLine(); + writer.flush(); + + // The response is a length prefixed stream of ASCII bytes. + int numBytes = inputStream.readInt(); + byte[] bytes = new byte[numBytes]; + inputStream.readFully(bytes); + + return new String(bytes, StandardCharsets.US_ASCII); } /** @@ -677,30 +655,16 @@ public class Process { } /** - * Returns the number of times we attempt a connection to the zygote. We - * sleep for {@link #ZYGOTE_RETRY_MILLIS} milliseconds between each try. - * - * This could probably be removed, see TODO in {@code ZygoteState#connect}. - */ - private static int getNumTries(ZygoteState state) { - // Retry 10 times for the first connection to each zygote. - if (state == null) { - return 11; - } - - // This means the connection has already been established, but subsequently - // closed, possibly due to an IOException. We retry just once if that's the - // case. - return 1; - } - - /** * Tries to open socket to Zygote process if not already open. If * already open, does nothing. May block and retry. */ private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { if (primaryZygoteState == null || primaryZygoteState.isClosed()) { - primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET, getNumTries(primaryZygoteState)); + try { + primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET); + } catch (IOException ioe) { + throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe); + } } if (primaryZygoteState.matches(abi)) { @@ -709,8 +673,11 @@ public class Process { // The primary zygote didn't match. Try the secondary. if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { - secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET, - getNumTries(secondaryZygoteState)); + try { + secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET); + } catch (IOException ioe) { + throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe); + } } if (secondaryZygoteState.matches(abi)) { diff --git a/core/java/android/os/RemoteException.java b/core/java/android/os/RemoteException.java index e30d24f..98d7523 100644 --- a/core/java/android/os/RemoteException.java +++ b/core/java/android/os/RemoteException.java @@ -28,4 +28,9 @@ public class RemoteException extends AndroidException { public RemoteException(String message) { super(message); } + + /** {@hide} */ + public RuntimeException rethrowAsRuntimeException() { + throw new RuntimeException(this); + } } diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 57ed979..474192f 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -70,6 +70,8 @@ public final class Trace { public static final long TRACE_TAG_DALVIK = 1L << 14; /** @hide */ public static final long TRACE_TAG_RS = 1L << 15; + /** @hide */ + public static final long TRACE_TAG_BIONIC = 1L << 16; private static final long TRACE_TAG_NOT_READY = 1L << 63; private static final int MAX_SECTION_NAME_LEN = 127; diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 312cdbe..ee219e3 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -266,6 +266,17 @@ public class UserManager { */ public static final String DISALLOW_ADJUST_VOLUME = "no_adjust_volume"; + /** + * Key for user restrictions. Specifies that the user is not allowed to send or receive + * phone calls or text messages. Emergency calls may still be permitted. + * The default value is <code>false</code>. + * <p/> + * Type: Boolean + * @see #setUserRestrictions(Bundle) + * @see #getUserRestrictions() + */ + public static final String DISALLOW_TELEPHONY = "no_telephony"; + /** @hide */ public static final int PIN_VERIFICATION_FAILED_INCORRECT = -3; /** @hide */ diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 4963991..68b91cb 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -58,24 +58,6 @@ 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; @@ -663,4 +645,14 @@ public class StorageManager { return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES, DEFAULT_FULL_THRESHOLD_BYTES); } + + /// Consts to match the password types in cryptfs.h + /** @hide */ + public static final int CRYPT_TYPE_PASSWORD = 0; + /** @hide */ + public static final int CRYPT_TYPE_DEFAULT = 1; + /** @hide */ + public static final int CRYPT_TYPE_PATTERN = 2; + /** @hide */ + public static final int CRYPT_TYPE_PIN = 3; } diff --git a/core/java/android/preference/PreferenceManager.java b/core/java/android/preference/PreferenceManager.java index 5c8c8e9..ad940c6 100644 --- a/core/java/android/preference/PreferenceManager.java +++ b/core/java/android/preference/PreferenceManager.java @@ -621,8 +621,9 @@ public class PreferenceManager { * Registers a listener. * * @see OnActivityStopListener + * @hide */ - void registerOnActivityStopListener(OnActivityStopListener listener) { + public void registerOnActivityStopListener(OnActivityStopListener listener) { synchronized (this) { if (mActivityStopListeners == null) { mActivityStopListeners = new ArrayList<OnActivityStopListener>(); @@ -638,8 +639,9 @@ public class PreferenceManager { * Unregisters a listener. * * @see OnActivityStopListener + * @hide */ - void unregisterOnActivityStopListener(OnActivityStopListener listener) { + public void unregisterOnActivityStopListener(OnActivityStopListener listener) { synchronized (this) { if (mActivityStopListeners != null) { mActivityStopListeners.remove(listener); diff --git a/core/java/android/preference/SeekBarVolumizer.java b/core/java/android/preference/SeekBarVolumizer.java new file mode 100644 index 0000000..d66fc0f --- /dev/null +++ b/core/java/android/preference/SeekBarVolumizer.java @@ -0,0 +1,318 @@ +/* + * 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.preference; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.media.AudioManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Message; +import android.preference.VolumePreference.VolumeStore; +import android.provider.Settings; +import android.provider.Settings.System; +import android.util.Log; +import android.widget.SeekBar; +import android.widget.SeekBar.OnSeekBarChangeListener; + +/** + * Turns a {@link SeekBar} into a volume control. + * @hide + */ +public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback { + private static final String TAG = "SeekBarVolumizer"; + + public interface Callback { + void onSampleStarting(SeekBarVolumizer sbv); + } + + private final Context mContext; + private final Handler mHandler; + private final H mUiHandler = new H(); + private final Callback mCallback; + private final Uri mDefaultUri; + private final AudioManager mAudioManager; + private final int mStreamType; + private final int mMaxStreamVolume; + private final Receiver mReceiver = new Receiver(); + private final Observer mVolumeObserver; + + private int mOriginalStreamVolume; + private Ringtone mRingtone; + private int mLastProgress = -1; + private SeekBar mSeekBar; + private int mVolumeBeforeMute = -1; + + private static final int MSG_SET_STREAM_VOLUME = 0; + private static final int MSG_START_SAMPLE = 1; + private static final int MSG_STOP_SAMPLE = 2; + private static final int MSG_INIT_SAMPLE = 3; + private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000; + + public SeekBarVolumizer(Context context, int streamType, Uri defaultUri, + Callback callback) { + mContext = context; + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mStreamType = streamType; + mMaxStreamVolume = mAudioManager.getStreamMaxVolume(mStreamType); + HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); + thread.start(); + mHandler = new Handler(thread.getLooper(), this); + mCallback = callback; + mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType); + mVolumeObserver = new Observer(mHandler); + mContext.getContentResolver().registerContentObserver( + System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), + false, mVolumeObserver); + mReceiver.setListening(true); + if (defaultUri == null) { + if (mStreamType == AudioManager.STREAM_RING) { + defaultUri = Settings.System.DEFAULT_RINGTONE_URI; + } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) { + defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; + } else { + defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI; + } + } + mDefaultUri = defaultUri; + mHandler.sendEmptyMessage(MSG_INIT_SAMPLE); + } + + public void setSeekBar(SeekBar seekBar) { + if (mSeekBar != null) { + mSeekBar.setOnSeekBarChangeListener(null); + } + mSeekBar = seekBar; + mSeekBar.setOnSeekBarChangeListener(null); + mSeekBar.setMax(mMaxStreamVolume); + mSeekBar.setProgress(mLastProgress > -1 ? mLastProgress : mOriginalStreamVolume); + mSeekBar.setOnSeekBarChangeListener(this); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_SET_STREAM_VOLUME: + mAudioManager.setStreamVolume(mStreamType, mLastProgress, 0); + break; + case MSG_START_SAMPLE: + onStartSample(); + break; + case MSG_STOP_SAMPLE: + onStopSample(); + break; + case MSG_INIT_SAMPLE: + onInitSample(); + break; + default: + Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what); + } + return true; + } + + private void onInitSample() { + mRingtone = RingtoneManager.getRingtone(mContext, mDefaultUri); + if (mRingtone != null) { + mRingtone.setStreamType(mStreamType); + } + } + + private void postStartSample() { + mHandler.removeMessages(MSG_START_SAMPLE); + mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE), + isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0); + } + + private void onStartSample() { + if (!isSamplePlaying()) { + if (mCallback != null) { + mCallback.onSampleStarting(this); + } + if (mRingtone != null) { + try { + mRingtone.play(); + } catch (Throwable e) { + Log.w(TAG, "Error playing ringtone, stream " + mStreamType, e); + } + } + } + } + + void postStopSample() { + // remove pending delayed start messages + mHandler.removeMessages(MSG_START_SAMPLE); + mHandler.removeMessages(MSG_STOP_SAMPLE); + mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE)); + } + + private void onStopSample() { + if (mRingtone != null) { + mRingtone.stop(); + } + } + + public void stop() { + postStopSample(); + mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); + mSeekBar.setOnSeekBarChangeListener(null); + mReceiver.setListening(false); + mHandler.getLooper().quitSafely(); + } + + public void revertVolume() { + mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); + } + + public void onProgressChanged(SeekBar seekBar, int progress, + boolean fromTouch) { + if (!fromTouch) { + return; + } + + postSetVolume(progress); + } + + void postSetVolume(int progress) { + // Do the volume changing separately to give responsive UI + mLastProgress = progress; + mHandler.removeMessages(MSG_SET_STREAM_VOLUME); + mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME)); + } + + public void onStartTrackingTouch(SeekBar seekBar) { + } + + public void onStopTrackingTouch(SeekBar seekBar) { + postStartSample(); + } + + public boolean isSamplePlaying() { + return mRingtone != null && mRingtone.isPlaying(); + } + + public void startSample() { + postStartSample(); + } + + public void stopSample() { + postStopSample(); + } + + public SeekBar getSeekBar() { + return mSeekBar; + } + + public void changeVolumeBy(int amount) { + mSeekBar.incrementProgressBy(amount); + postSetVolume(mSeekBar.getProgress()); + postStartSample(); + mVolumeBeforeMute = -1; + } + + public void muteVolume() { + if (mVolumeBeforeMute != -1) { + mSeekBar.setProgress(mVolumeBeforeMute); + postSetVolume(mVolumeBeforeMute); + postStartSample(); + mVolumeBeforeMute = -1; + } else { + mVolumeBeforeMute = mSeekBar.getProgress(); + mSeekBar.setProgress(0); + postStopSample(); + postSetVolume(0); + } + } + + public void onSaveInstanceState(VolumeStore volumeStore) { + if (mLastProgress >= 0) { + volumeStore.volume = mLastProgress; + volumeStore.originalVolume = mOriginalStreamVolume; + } + } + + public void onRestoreInstanceState(VolumeStore volumeStore) { + if (volumeStore.volume != -1) { + mOriginalStreamVolume = volumeStore.originalVolume; + mLastProgress = volumeStore.volume; + postSetVolume(mLastProgress); + } + } + + private final class H extends Handler { + private static final int UPDATE_SLIDER = 1; + + @Override + public void handleMessage(Message msg) { + if (msg.what == UPDATE_SLIDER) { + if (mSeekBar != null) { + mSeekBar.setProgress(msg.arg1); + mLastProgress = mSeekBar.getProgress(); + } + } + } + + public void postUpdateSlider(int volume) { + obtainMessage(UPDATE_SLIDER, volume, 0).sendToTarget(); + } + } + + private final class Observer extends ContentObserver { + public Observer(Handler handler) { + super(handler); + } + + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + if (mSeekBar != null && mAudioManager != null) { + final int volume = mAudioManager.getStreamVolume(mStreamType); + mUiHandler.postUpdateSlider(volume); + } + } + } + + private final class Receiver extends BroadcastReceiver { + private boolean mListening; + + public void setListening(boolean listening) { + if (mListening == listening) return; + mListening = listening; + if (listening) { + final IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION); + mContext.registerReceiver(this, filter); + } else { + mContext.unregisterReceiver(this); + } + } + + @Override + public void onReceive(Context context, Intent intent) { + if (!AudioManager.VOLUME_CHANGED_ACTION.equals(intent.getAction())) return; + final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1); + final int streamValue = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, -1); + if (mSeekBar != null && streamType == mStreamType && streamValue != -1) { + mUiHandler.postUpdateSlider(streamValue); + } + } + } +} diff --git a/core/java/android/preference/VolumePreference.java b/core/java/android/preference/VolumePreference.java index 29f2545..df9e10e 100644 --- a/core/java/android/preference/VolumePreference.java +++ b/core/java/android/preference/VolumePreference.java @@ -19,32 +19,20 @@ package android.preference; import android.app.Dialog; import android.content.Context; import android.content.res.TypedArray; -import android.database.ContentObserver; -import android.media.AudioManager; -import android.media.Ringtone; -import android.media.RingtoneManager; -import android.net.Uri; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.Message; import android.os.Parcel; import android.os.Parcelable; -import android.provider.Settings; -import android.provider.Settings.System; import android.util.AttributeSet; -import android.util.Log; import android.view.KeyEvent; import android.view.View; import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; /** * @hide */ public class VolumePreference extends SeekBarDialogPreference implements - PreferenceManager.OnActivityStopListener, View.OnKeyListener { + PreferenceManager.OnActivityStopListener, View.OnKeyListener, SeekBarVolumizer.Callback { - private static final String TAG = "VolumePreference"; + static final String TAG = "VolumePreference"; private int mStreamType; @@ -66,7 +54,7 @@ public class VolumePreference extends SeekBarDialogPreference implements } public VolumePreference(Context context, AttributeSet attrs) { - this(context, attrs, 0); + this(context, attrs, com.android.internal.R.attr.dialogPreferenceStyle); } public void setStreamType(int streamType) { @@ -78,7 +66,8 @@ public class VolumePreference extends SeekBarDialogPreference implements super.onBindDialogView(view); final SeekBar seekBar = (SeekBar) view.findViewById(com.android.internal.R.id.seekbar); - mSeekBarVolumizer = new SeekBarVolumizer(getContext(), seekBar, mStreamType); + mSeekBarVolumizer = new SeekBarVolumizer(getContext(), mStreamType, null, this); + mSeekBarVolumizer.setSeekBar(seekBar); getPreferenceManager().registerOnActivityStopListener(this); @@ -152,7 +141,8 @@ public class VolumePreference extends SeekBarDialogPreference implements } - protected void onSampleStarting(SeekBarVolumizer volumizer) { + @Override + public void onSampleStarting(SeekBarVolumizer volumizer) { if (mSeekBarVolumizer != null && volumizer != mSeekBarVolumizer) { mSeekBarVolumizer.stopSample(); } @@ -228,213 +218,4 @@ public class VolumePreference extends SeekBarDialogPreference implements } }; } - - /** - * Turns a {@link SeekBar} into a volume control. - */ - public class SeekBarVolumizer implements OnSeekBarChangeListener, Handler.Callback { - - private Context mContext; - private Handler mHandler; - - private AudioManager mAudioManager; - private int mStreamType; - private int mOriginalStreamVolume; - private Ringtone mRingtone; - - private int mLastProgress = -1; - private SeekBar mSeekBar; - private int mVolumeBeforeMute = -1; - - private static final int MSG_SET_STREAM_VOLUME = 0; - private static final int MSG_START_SAMPLE = 1; - private static final int MSG_STOP_SAMPLE = 2; - private static final int CHECK_RINGTONE_PLAYBACK_DELAY_MS = 1000; - - private ContentObserver mVolumeObserver = new ContentObserver(mHandler) { - @Override - public void onChange(boolean selfChange) { - super.onChange(selfChange); - if (mSeekBar != null && mAudioManager != null) { - int volume = mAudioManager.getStreamVolume(mStreamType); - mSeekBar.setProgress(volume); - } - } - }; - - public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType) { - this(context, seekBar, streamType, null); - } - - public SeekBarVolumizer(Context context, SeekBar seekBar, int streamType, Uri defaultUri) { - mContext = context; - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - mStreamType = streamType; - mSeekBar = seekBar; - - HandlerThread thread = new HandlerThread(TAG + ".CallbackHandler"); - thread.start(); - mHandler = new Handler(thread.getLooper(), this); - - initSeekBar(seekBar, defaultUri); - } - - private void initSeekBar(SeekBar seekBar, Uri defaultUri) { - seekBar.setMax(mAudioManager.getStreamMaxVolume(mStreamType)); - mOriginalStreamVolume = mAudioManager.getStreamVolume(mStreamType); - seekBar.setProgress(mOriginalStreamVolume); - seekBar.setOnSeekBarChangeListener(this); - - mContext.getContentResolver().registerContentObserver( - System.getUriFor(System.VOLUME_SETTINGS[mStreamType]), - false, mVolumeObserver); - - if (defaultUri == null) { - if (mStreamType == AudioManager.STREAM_RING) { - defaultUri = Settings.System.DEFAULT_RINGTONE_URI; - } else if (mStreamType == AudioManager.STREAM_NOTIFICATION) { - defaultUri = Settings.System.DEFAULT_NOTIFICATION_URI; - } else { - defaultUri = Settings.System.DEFAULT_ALARM_ALERT_URI; - } - } - - mRingtone = RingtoneManager.getRingtone(mContext, defaultUri); - - if (mRingtone != null) { - mRingtone.setStreamType(mStreamType); - } - } - - @Override - public boolean handleMessage(Message msg) { - switch (msg.what) { - case MSG_SET_STREAM_VOLUME: - mAudioManager.setStreamVolume(mStreamType, mLastProgress, 0); - break; - case MSG_START_SAMPLE: - onStartSample(); - break; - case MSG_STOP_SAMPLE: - onStopSample(); - break; - default: - Log.e(TAG, "invalid SeekBarVolumizer message: "+msg.what); - } - return true; - } - - private void postStartSample() { - mHandler.removeMessages(MSG_START_SAMPLE); - mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_START_SAMPLE), - isSamplePlaying() ? CHECK_RINGTONE_PLAYBACK_DELAY_MS : 0); - } - - private void onStartSample() { - if (!isSamplePlaying()) { - onSampleStarting(this); - if (mRingtone != null) { - mRingtone.play(); - } - } - } - - private void postStopSample() { - // remove pending delayed start messages - mHandler.removeMessages(MSG_START_SAMPLE); - mHandler.removeMessages(MSG_STOP_SAMPLE); - mHandler.sendMessage(mHandler.obtainMessage(MSG_STOP_SAMPLE)); - } - - private void onStopSample() { - if (mRingtone != null) { - mRingtone.stop(); - } - } - - public void stop() { - postStopSample(); - mContext.getContentResolver().unregisterContentObserver(mVolumeObserver); - mSeekBar.setOnSeekBarChangeListener(null); - } - - public void revertVolume() { - mAudioManager.setStreamVolume(mStreamType, mOriginalStreamVolume, 0); - } - - public void onProgressChanged(SeekBar seekBar, int progress, - boolean fromTouch) { - if (!fromTouch) { - return; - } - - postSetVolume(progress); - } - - void postSetVolume(int progress) { - // Do the volume changing separately to give responsive UI - mLastProgress = progress; - mHandler.removeMessages(MSG_SET_STREAM_VOLUME); - mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_STREAM_VOLUME)); - } - - public void onStartTrackingTouch(SeekBar seekBar) { - } - - public void onStopTrackingTouch(SeekBar seekBar) { - postStartSample(); - } - - public boolean isSamplePlaying() { - return mRingtone != null && mRingtone.isPlaying(); - } - - public void startSample() { - postStartSample(); - } - - public void stopSample() { - postStopSample(); - } - - public SeekBar getSeekBar() { - return mSeekBar; - } - - public void changeVolumeBy(int amount) { - mSeekBar.incrementProgressBy(amount); - postSetVolume(mSeekBar.getProgress()); - postStartSample(); - mVolumeBeforeMute = -1; - } - - public void muteVolume() { - if (mVolumeBeforeMute != -1) { - mSeekBar.setProgress(mVolumeBeforeMute); - postSetVolume(mVolumeBeforeMute); - postStartSample(); - mVolumeBeforeMute = -1; - } else { - mVolumeBeforeMute = mSeekBar.getProgress(); - mSeekBar.setProgress(0); - postStopSample(); - postSetVolume(0); - } - } - - public void onSaveInstanceState(VolumeStore volumeStore) { - if (mLastProgress >= 0) { - volumeStore.volume = mLastProgress; - volumeStore.originalVolume = mOriginalStreamVolume; - } - } - - public void onRestoreInstanceState(VolumeStore volumeStore) { - if (volumeStore.volume != -1) { - mOriginalStreamVolume = volumeStore.originalVolume; - mLastProgress = volumeStore.volume; - postSetVolume(mLastProgress); - } - } - } } diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 11678a6..6db78f4 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -903,8 +903,6 @@ public final class ContactsContract { /** * 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"; @@ -1154,8 +1152,6 @@ public final class ContactsContract { * address book index, which is usually the first letter of the sort key. * When this parameter is supplied, the row counts are returned in the * cursor extras bundle. - * - * @hide */ public final static class ContactCounts { @@ -1165,7 +1161,24 @@ public final class ContactsContract { * first letter of the sort key. This parameter does not affect the main * content of the cursor. * - * @hide + * <p> + * <pre> + * Example: + * Uri uri = Contacts.CONTENT_URI.buildUpon() + * .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true") + * .build(); + * Cursor cursor = getContentResolver().query(uri, + * new String[] {Contacts.DISPLAY_NAME}, + * null, null, null); + * Bundle bundle = cursor.getExtras(); + * if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES) && + * bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) { + * String sections[] = + * bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); + * int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); + * } + * </pre> + * </p> */ public static final String ADDRESS_BOOK_INDEX_EXTRAS = "address_book_index_extras"; @@ -1173,8 +1186,6 @@ public final class ContactsContract { * The array of address book index titles, which are returned in the * same order as the data in the cursor. * <p>TYPE: String[]</p> - * - * @hide */ public static final String EXTRA_ADDRESS_BOOK_INDEX_TITLES = "address_book_index_titles"; @@ -1182,8 +1193,6 @@ public final class ContactsContract { * The array of group counts for the corresponding group. Contains the same number * of elements as the EXTRA_ADDRESS_BOOK_INDEX_TITLES array. * <p>TYPE: int[]</p> - * - * @hide */ public static final String EXTRA_ADDRESS_BOOK_INDEX_COUNTS = "address_book_index_counts"; } diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index b907375..6b8e2de 100644 --- a/core/java/android/provider/DocumentsContract.java +++ b/core/java/android/provider/DocumentsContract.java @@ -287,6 +287,16 @@ public final class DocumentsContract { public static final int FLAG_DIR_PREFERS_LAST_MODIFIED = 1 << 5; /** + * Flag indicating that a document can be renamed. + * + * @see #COLUMN_FLAGS + * @see DocumentsContract#renameDocument(ContentProviderClient, Uri, + * String) + * @see DocumentsProvider#renameDocument(String, String) + */ + public static final int FLAG_SUPPORTS_RENAME = 1 << 6; + + /** * Flag indicating that document titles should be hidden when viewing * this directory in a larger format grid. For example, a directory * containing only images may want the image thumbnails to speak for @@ -494,6 +504,8 @@ public final class DocumentsContract { /** {@hide} */ public static final String METHOD_CREATE_DOCUMENT = "android:createDocument"; /** {@hide} */ + public static final String METHOD_RENAME_DOCUMENT = "android:renameDocument"; + /** {@hide} */ public static final String METHOD_DELETE_DOCUMENT = "android:deleteDocument"; /** {@hide} */ @@ -898,6 +910,45 @@ public final class DocumentsContract { } /** + * Change the display name of an existing document. + * <p> + * If the underlying provider needs to create a new + * {@link Document#COLUMN_DOCUMENT_ID} to represent the updated display + * name, that new document is returned and the original document is no + * longer valid. Otherwise, the original document is returned. + * + * @param documentUri document with {@link Document#FLAG_SUPPORTS_RENAME} + * @param displayName updated name for document + * @return the existing or new document after the rename, or {@code null} if + * failed. + */ + public static Uri renameDocument(ContentResolver resolver, Uri documentUri, + String displayName) { + final ContentProviderClient client = resolver.acquireUnstableContentProviderClient( + documentUri.getAuthority()); + try { + return renameDocument(client, documentUri, displayName); + } catch (Exception e) { + Log.w(TAG, "Failed to rename document", e); + return null; + } finally { + ContentProviderClient.releaseQuietly(client); + } + } + + /** {@hide} */ + public static Uri renameDocument(ContentProviderClient client, Uri documentUri, + String displayName) throws RemoteException { + final Bundle in = new Bundle(); + in.putParcelable(DocumentsContract.EXTRA_URI, documentUri); + in.putString(Document.COLUMN_DISPLAY_NAME, displayName); + + final Bundle out = client.call(METHOD_RENAME_DOCUMENT, null, in); + final Uri outUri = out.getParcelable(DocumentsContract.EXTRA_URI); + return (outUri != null) ? outUri : documentUri; + } + + /** * Delete the given document. * * @param documentUri document with {@link Document#FLAG_SUPPORTS_DELETE} diff --git a/core/java/android/provider/DocumentsProvider.java b/core/java/android/provider/DocumentsProvider.java index 1a7a00f2..066b4aa 100644 --- a/core/java/android/provider/DocumentsProvider.java +++ b/core/java/android/provider/DocumentsProvider.java @@ -19,9 +19,11 @@ package android.provider; import static android.provider.DocumentsContract.EXTRA_THUMBNAIL_SIZE; import static android.provider.DocumentsContract.METHOD_CREATE_DOCUMENT; import static android.provider.DocumentsContract.METHOD_DELETE_DOCUMENT; +import static android.provider.DocumentsContract.METHOD_RENAME_DOCUMENT; import static android.provider.DocumentsContract.getDocumentId; import static android.provider.DocumentsContract.getRootId; import static android.provider.DocumentsContract.getSearchDocumentsQuery; +import static android.provider.DocumentsContract.isViaUri; import android.content.ContentProvider; import android.content.ContentResolver; @@ -206,7 +208,7 @@ public abstract class DocumentsProvider extends ContentProvider { * If the MIME type is not supported, the provider must throw. * @param displayName the display name of the new document. The provider may * alter this name to meet any internal constraints, such as - * conflicting names. + * avoiding conflicting names. */ @SuppressWarnings("unused") public String createDocument(String parentDocumentId, String mimeType, String displayName) @@ -215,11 +217,33 @@ public abstract class DocumentsProvider extends ContentProvider { } /** - * Delete the requested document. Upon returning, any URI permission grants - * 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)}. + * Rename an existing document. + * <p> + * If a different {@link Document#COLUMN_DOCUMENT_ID} must be used to + * represent the renamed document, generate and return it. Any outstanding + * URI permission grants will be updated to point at the new document. If + * the original {@link Document#COLUMN_DOCUMENT_ID} is still valid after the + * rename, return {@code null}. + * + * @param documentId the document to rename. + * @param displayName the updated display name of the document. The provider + * may alter this name to meet any internal constraints, such as + * avoiding conflicting names. + */ + @SuppressWarnings("unused") + public String renameDocument(String documentId, String displayName) + throws FileNotFoundException { + throw new UnsupportedOperationException("Rename not supported"); + } + + /** + * Delete the requested document. + * <p> + * Upon returning, any URI permission grants 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. */ @@ -523,26 +547,33 @@ public abstract class DocumentsProvider extends ContentProvider { 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; - } + // the narrow URI. + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, uri); context.grantUriPermission(getCallingPackage(), narrowUri, modeFlags); return narrowUri; } return null; } + private static int getCallingOrSelfUriPermissionModeFlags(Context context, Uri uri) { + // TODO: move this to a direct AMS call + int modeFlags = 0; + if (context.checkCallingOrSelfUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + == PackageManager.PERMISSION_GRANTED) { + 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; + } + return modeFlags; + } + /** * Implementation is provided by the parent class. Throws by default, and * cannot be overriden. @@ -588,6 +619,7 @@ public abstract class DocumentsProvider extends ContentProvider { return super.call(method, arg, extras); } + final Context context = getContext(); final Uri documentUri = extras.getParcelable(DocumentsContract.EXTRA_URI); final String authority = documentUri.getAuthority(); final String documentId = DocumentsContract.getDocumentId(documentUri); @@ -605,7 +637,6 @@ public abstract class DocumentsProvider extends ContentProvider { 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); // No need to issue new grants here, since caller either has @@ -615,6 +646,30 @@ public abstract class DocumentsProvider extends ContentProvider { newDocumentId); out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + } else if (METHOD_RENAME_DOCUMENT.equals(method)) { + enforceWritePermissionInner(documentUri); + + final String displayName = extras.getString(Document.COLUMN_DISPLAY_NAME); + final String newDocumentId = renameDocument(documentId, displayName); + + if (newDocumentId != null) { + final Uri newDocumentUri = DocumentsContract.buildDocumentMaybeViaUri( + documentUri, newDocumentId); + + // If caller came in with a narrow grant, issue them a + // narrow grant for the newly renamed document. + if (!isViaUri(newDocumentUri)) { + final int modeFlags = getCallingOrSelfUriPermissionModeFlags(context, + documentUri); + context.grantUriPermission(getCallingPackage(), newDocumentUri, modeFlags); + } + + out.putParcelable(DocumentsContract.EXTRA_URI, newDocumentUri); + + // Original document no longer exists, clean up any grants + revokeDocumentPermission(documentId); + } + } else if (METHOD_DELETE_DOCUMENT.equals(method)) { enforceWritePermissionInner(documentUri); deleteDocument(documentId); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1847b55..bec401e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -769,6 +769,28 @@ public final class Settings { public static final String ACTION_SHOW_REGULATORY_INFO = "android.settings.SHOW_REGULATORY_INFO"; + /** + * Activity Action: Show Device Name Settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String DEVICE_NAME_SETTINGS = "android.settings.DEVICE_NAME"; + + /** + * Activity Action: Show pairing settings. + * <p> + * In some cases, a matching Activity may not exist, so ensure you safeguard + * against this. + * + * @hide + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_PAIRING_SETTINGS = "android.settings.PAIRING_SETTINGS"; + // End of Intent actions for Settings /** @@ -1066,6 +1088,9 @@ public final class Settings { MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_COUNT); MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_DELAY_MS); MOVED_TO_SECURE.add(Secure.WIFI_WATCHDOG_PING_TIMEOUT_MS); + + // At one time in System, then Global, but now back in Secure + MOVED_TO_SECURE.add(Secure.INSTALL_NON_MARKET_APPS); } private static final HashSet<String> MOVED_TO_GLOBAL; @@ -1080,7 +1105,6 @@ public final class Settings { MOVED_TO_SECURE_THEN_GLOBAL.add(Global.BLUETOOTH_ON); MOVED_TO_SECURE_THEN_GLOBAL.add(Global.DATA_ROAMING); MOVED_TO_SECURE_THEN_GLOBAL.add(Global.DEVICE_PROVISIONED); - MOVED_TO_SECURE_THEN_GLOBAL.add(Global.INSTALL_NON_MARKET_APPS); MOVED_TO_SECURE_THEN_GLOBAL.add(Global.USB_MASS_STORAGE_ENABLED); MOVED_TO_SECURE_THEN_GLOBAL.add(Global.HTTP_PROXY); @@ -2503,6 +2527,14 @@ public final class Settings { NOTIFICATION_SOUND }; + /** + * When to use Wi-Fi calling + * + * @see android.telephony.TelephonyManager.WifiCallingChoices + * @hide + */ + public static final String WHEN_TO_MAKE_WIFI_CALLS = "when_to_make_wifi_calls"; + // Settings moved to Settings.Secure /** @@ -2543,10 +2575,10 @@ public final class Settings { public static final String HTTP_PROXY = Global.HTTP_PROXY; /** - * @deprecated Use {@link android.provider.Settings.Global#INSTALL_NON_MARKET_APPS} instead + * @deprecated Use {@link android.provider.Settings.Secure#INSTALL_NON_MARKET_APPS} instead */ @Deprecated - public static final String INSTALL_NON_MARKET_APPS = Global.INSTALL_NON_MARKET_APPS; + public static final String INSTALL_NON_MARKET_APPS = Secure.INSTALL_NON_MARKET_APPS; /** * @deprecated Use {@link android.provider.Settings.Secure#LOCATION_PROVIDERS_ALLOWED} @@ -2784,7 +2816,6 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.DISPLAY_SIZE_FORCED); MOVED_TO_GLOBAL.add(Settings.Global.DOWNLOAD_MAX_BYTES_OVER_MOBILE); MOVED_TO_GLOBAL.add(Settings.Global.DOWNLOAD_RECOMMENDED_MAX_BYTES_OVER_MOBILE); - MOVED_TO_GLOBAL.add(Settings.Global.INSTALL_NON_MARKET_APPS); MOVED_TO_GLOBAL.add(Settings.Global.MOBILE_DATA); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_DEV_BUCKET_DURATION); MOVED_TO_GLOBAL.add(Settings.Global.NETSTATS_DEV_DELETE_AGE); @@ -2881,6 +2912,7 @@ public final class Settings { MOVED_TO_GLOBAL.add(Settings.Global.SET_GLOBAL_HTTP_PROXY); MOVED_TO_GLOBAL.add(Settings.Global.DEFAULT_DNS_SERVER); MOVED_TO_GLOBAL.add(Settings.Global.PREFERRED_NETWORK_MODE); + MOVED_TO_GLOBAL.add(Settings.Global.WEBVIEW_DATA_REDUCTION_PROXY_KEY); } /** @hide */ @@ -3373,10 +3405,13 @@ public final class Settings { public static final String HTTP_PROXY = Global.HTTP_PROXY; /** - * @deprecated Use {@link android.provider.Settings.Global#INSTALL_NON_MARKET_APPS} instead + * Whether applications can be installed for this user via the system's + * {@link Intent#ACTION_INSTALL_PACKAGE} mechanism. + * + * <p>1 = permit app installation via the system package installer intent + * <p>0 = do not allow use of the package installer */ - @Deprecated - public static final String INSTALL_NON_MARKET_APPS = Global.INSTALL_NON_MARKET_APPS; + public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; /** * Comma-separated list of location providers that activities may access. Do not rely on @@ -3442,11 +3477,6 @@ 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) * @@ -3838,22 +3868,11 @@ public final class Settings { /** * 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. * @@ -3881,44 +3900,6 @@ public final class Settings { "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 */ @@ -4480,6 +4461,12 @@ public final class Settings { INCALL_POWER_BUTTON_BEHAVIOR_SCREEN_OFF; /** + * Whether the device should wake when the wake gesture sensor detects motion. + * @hide + */ + public static final String WAKE_GESTURE_ENABLED = "wake_gesture_enabled"; + + /** * The current night mode that has been selected by the user. Owned * and controlled by UiModeManagerService. Constants are as per * UiModeManager. @@ -4574,6 +4561,12 @@ public final class Settings { public static final String PAYMENT_SERVICE_SEARCH_URI = "payment_service_search_uri"; /** + * If enabled, intercepted notifications will be displayed (not suppressed) in zen mode. + * @hide + */ + public static final String DISPLAY_INTERCEPTED_NOTIFICATIONS = "display_intercepted_notifications"; + + /** * This are the settings to be backed up. * * NOTE: Settings are backed up and restored in the order they appear @@ -5078,13 +5071,10 @@ public final class Settings { "download_manager_recommended_max_bytes_over_mobile"; /** - * Whether the package installer should allow installation of apps downloaded from - * sources other than Google Play. - * - * 1 = allow installing from other sources - * 0 = only allow installing from Google Play + * @deprecated Use {@link android.provider.Settings.Secure#INSTALL_NON_MARKET_APPS} instead */ - public static final String INSTALL_NON_MARKET_APPS = "install_non_market_apps"; + @Deprecated + public static final String INSTALL_NON_MARKET_APPS = Secure.INSTALL_NON_MARKET_APPS; /** * Whether mobile data connections are allowed by the user. See @@ -5360,6 +5350,13 @@ public final class Settings { */ public static final String USE_GOOGLE_MAIL = "use_google_mail"; + /** + * Webview Data reduction proxy key. + * @hide + */ + public static final String WEBVIEW_DATA_REDUCTION_PROXY_KEY = + "webview_data_reduction_proxy_key"; + /** * Whether Wifi display is enabled/disabled * 0=disabled. 1=enabled. @@ -6196,6 +6193,13 @@ public final class Settings { /** @hide */ public static final int HEADS_UP_ON = 1; /** + * The name of the device + * + * @hide + */ + public static final String DEVICE_NAME = "device_name"; + + /** * Settings to backup. This is here so that it's in the same place as the settings * keys and easy to update. * @@ -6236,6 +6240,13 @@ public final class Settings { CALL_METHOD_GET_GLOBAL, CALL_METHOD_PUT_GLOBAL); + // Certain settings have been moved from global to the per-user secure namespace + private static final HashSet<String> MOVED_TO_SECURE; + static { + MOVED_TO_SECURE = new HashSet<String>(1); + MOVED_TO_SECURE.add(Settings.Global.INSTALL_NON_MARKET_APPS); + } + /** * Look up a name in the database. * @param resolver to access the database with @@ -6249,6 +6260,11 @@ public final class Settings { /** @hide */ public static String getStringForUser(ContentResolver resolver, String name, int userHandle) { + if (MOVED_TO_SECURE.contains(name)) { + Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Global" + + " to android.provider.Settings.Secure, returning read-only value."); + return Secure.getStringForUser(resolver, name, userHandle); + } return sNameValueCache.getStringForUser(resolver, name, userHandle); } @@ -6271,6 +6287,12 @@ public final class Settings { Log.v(TAG, "Global.putString(name=" + name + ", value=" + value + " for " + userHandle); } + // Global and Secure have the same access policy so we can forward writes + if (MOVED_TO_SECURE.contains(name)) { + Log.w(TAG, "Setting " + name + " has moved from android.provider.Settings.Global" + + " to android.provider.Settings.Secure, value is unchanged."); + return Secure.putStringForUser(resolver, name, value, userHandle); + } return sNameValueCache.putStringForUser(resolver, name, value, userHandle); } diff --git a/core/java/android/provider/TvContract.java b/core/java/android/provider/TvContract.java deleted file mode 100644 index 62252be..0000000 --- a/core/java/android/provider/TvContract.java +++ /dev/null @@ -1,596 +0,0 @@ -/* - * 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/fingerprint/FingerprintManager.java b/core/java/android/service/fingerprint/FingerprintManager.java index 0d14c59..2fcec52 100644 --- a/core/java/android/service/fingerprint/FingerprintManager.java +++ b/core/java/android/service/fingerprint/FingerprintManager.java @@ -18,6 +18,7 @@ package android.service.fingerprint; import android.app.ActivityManagerNative; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; @@ -25,6 +26,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.Settings; import android.util.Log; /** @@ -33,7 +35,7 @@ import android.util.Log; public class FingerprintManager { private static final String TAG = "FingerprintManager"; - protected static final boolean DEBUG = true; + private static final boolean DEBUG = true; private static final String FINGERPRINT_SERVICE_PACKAGE = "com.android.service.fingerprint"; private static final String FINGERPRINT_SERVICE_CLASS = "com.android.service.fingerprint.FingerprintService"; @@ -58,6 +60,7 @@ public class FingerprintManager { private IFingerprintService mService; private FingerprintManagerReceiver mClientReceiver; + private Context mContext; private Handler mHandler = new Handler() { public void handleMessage(android.os.Message msg) { @@ -80,6 +83,7 @@ public class FingerprintManager { }; public FingerprintManager(Context context) { + mContext = context; // Connect to service... Intent intent = new Intent(); intent.setClassName(FINGERPRINT_SERVICE_PACKAGE, FINGERPRINT_SERVICE_CLASS); @@ -129,6 +133,17 @@ public class FingerprintManager { }; /** + * Determine whether the user has at least one fingerprint enrolled and enabled. + * + * @return true if at least one is enrolled and enabled + */ + public boolean enrolledAndEnabled() { + ContentResolver res = mContext.getContentResolver(); + return Settings.Secure.getInt(res, "fingerprint_enabled", 0) != 0 + && FingerprintUtils.getFingerprintIdsForUser(res, getCurrentUserId()).length > 0; + } + + /** * Start the enrollment process. Timeout dictates how long to wait for the user to * enroll a fingerprint. * diff --git a/core/java/android/service/notification/INotificationListener.aidl b/core/java/android/service/notification/INotificationListener.aidl index d4b29d8..b3705d8 100644 --- a/core/java/android/service/notification/INotificationListener.aidl +++ b/core/java/android/service/notification/INotificationListener.aidl @@ -17,11 +17,15 @@ package android.service.notification; import android.service.notification.StatusBarNotification; +import android.service.notification.NotificationRankingUpdate; /** @hide */ oneway interface INotificationListener { - void onListenerConnected(in String[] notificationKeys); - void onNotificationPosted(in StatusBarNotification notification); - void onNotificationRemoved(in StatusBarNotification notification); + void onListenerConnected(in NotificationRankingUpdate update); + void onNotificationPosted(in StatusBarNotification notification, + in NotificationRankingUpdate update); + void onNotificationRemoved(in StatusBarNotification notification, + in NotificationRankingUpdate update); + void onNotificationRankingUpdate(in NotificationRankingUpdate update); }
\ 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 3673f03..557f5a6 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -16,18 +16,26 @@ package android.service.notification; +import android.annotation.PrivateApi; import android.annotation.SdkConstant; import android.app.INotificationManager; import android.app.Service; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ParceledListSlice; import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; import android.os.ServiceManager; -import android.os.UserHandle; import android.util.Log; +import java.util.List; + /** - * A service that receives calls from the system when new notifications are posted or removed. + * A service that receives calls from the system when new notifications are + * posted or removed, or their ranking changed. * <p>To extend this class, you must declare the service in your manifest file with * the {@link android.Manifest.permission#BIND_NOTIFICATION_LISTENER_SERVICE} permission * and include an intent filter with the {@link #SERVICE_INTERFACE} action. For example:</p> @@ -46,9 +54,13 @@ public abstract class NotificationListenerService extends Service { + "[" + getClass().getSimpleName() + "]"; private INotificationListenerWrapper mWrapper = null; + private Ranking mRanking; private INotificationManager mNoMan; + /** Only valid after a successful call to (@link registerAsService}. */ + private int mCurrentUser; + /** * The {@link Intent} that must be declared as handled by the service. */ @@ -86,12 +98,19 @@ public abstract class NotificationListenerService extends Service { /** * Implement this method to learn about when the listener is enabled and connected to - * the notification manager. You are safe to call {@link #getActiveNotifications(String[]) + * the notification manager. You are safe to call {@link #getActiveNotifications()} * at this time. - * - * @param notificationKeys The notification keys for all currently posted notifications. */ - public void onListenerConnected(String[] notificationKeys) { + public void onListenerConnected() { + // optional + } + + /** + * Implement this method to be notified when the notification ranking changes. + * <P> + * Call {@link #getCurrentRanking()} to retrieve the new ranking. + */ + public void onNotificationRankingUpdate() { // optional } @@ -202,23 +221,15 @@ public abstract class NotificationListenerService extends Service { * Request the list of outstanding notifications (that is, those that are visible to the * current user). Useful when you don't know what's already been posted. * - * @return An array of active notifications. + * @return An array of active notifications, sorted in natural order. */ public StatusBarNotification[] getActiveNotifications() { - return getActiveNotifications(null /*all*/); - } - - /** - * Request the list of outstanding notifications (that is, those that are visible to the - * current user). Useful when you don't know what's already been posted. - * - * @param keys A specific list of notification keys, or {@code null} for all. - * @return An array of active notifications. - */ - public StatusBarNotification[] getActiveNotifications(String[] keys) { if (!isBound()) return null; try { - return getNotificationInterface().getActiveNotificationsFromListener(mWrapper, keys); + ParceledListSlice<StatusBarNotification> parceledList = + getNotificationInterface().getActiveNotificationsFromListener(mWrapper); + List<StatusBarNotification> list = parceledList.getList(); + return list.toArray(new StatusBarNotification[list.size()]); } catch (android.os.RemoteException ex) { Log.v(TAG, "Unable to contact notification manager", ex); } @@ -226,21 +237,20 @@ public abstract class NotificationListenerService extends Service { } /** - * 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[]). + * Returns current ranking information. + * + * <p> + * The returned object represents the current ranking snapshot and only + * applies for currently active notifications. Hence you must retrieve a + * new Ranking after each notification event such as + * {@link #onNotificationPosted(StatusBarNotification)}, + * {@link #onNotificationRemoved(StatusBarNotification)}, etc. * - * @return An array of active notification keys. + * @return A {@link NotificationListenerService.Ranking} object providing + * access to ranking information */ - 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; + public Ranking getCurrentRanking() { + return mRanking; } @Override @@ -259,30 +269,201 @@ public abstract class NotificationListenerService extends Service { return true; } + /** + * Directly register this service with the Notification Manager. + * + * <p>Only system services may use this call. It will fail for non-system callers. + * Apps should ask the user to add their listener in Settings. + * + * @param componentName the component that will consume the notification information + * @param currentUser the user to use as the stream filter + * @hide + */ + @PrivateApi + public void registerAsSystemService(ComponentName componentName, int currentUser) + throws RemoteException { + if (mWrapper == null) { + mWrapper = new INotificationListenerWrapper(); + } + INotificationManager noMan = getNotificationInterface(); + noMan.registerListener(mWrapper, componentName, currentUser); + mCurrentUser = currentUser; + } + + /** + * Directly unregister this service from the Notification Manager. + * + * <P>This method will fail for listeners that were not registered + * with (@link registerAsService). + * @hide + */ + @PrivateApi + public void unregisterAsSystemService() throws RemoteException { + if (mWrapper != null) { + INotificationManager noMan = getNotificationInterface(); + noMan.unregisterListener(mWrapper, mCurrentUser); + } + } + private class INotificationListenerWrapper extends INotificationListener.Stub { @Override - public void onNotificationPosted(StatusBarNotification sbn) { - try { - NotificationListenerService.this.onNotificationPosted(sbn); - } catch (Throwable t) { - Log.w(TAG, "Error running onNotificationPosted", t); + public void onNotificationPosted(StatusBarNotification sbn, + NotificationRankingUpdate update) { + // protect subclass from concurrent modifications of (@link mNotificationKeys}. + synchronized (mWrapper) { + applyUpdate(update); + try { + NotificationListenerService.this.onNotificationPosted(sbn); + } catch (Throwable t) { + Log.w(TAG, "Error running onNotificationPosted", t); + } } } @Override - public void onNotificationRemoved(StatusBarNotification sbn) { - try { - NotificationListenerService.this.onNotificationRemoved(sbn); - } catch (Throwable t) { - Log.w(TAG, "Error running onNotificationRemoved", t); + public void onNotificationRemoved(StatusBarNotification sbn, + NotificationRankingUpdate update) { + // protect subclass from concurrent modifications of (@link mNotificationKeys}. + synchronized (mWrapper) { + applyUpdate(update); + try { + NotificationListenerService.this.onNotificationRemoved(sbn); + } catch (Throwable t) { + Log.w(TAG, "Error running onNotificationRemoved", t); + } } } @Override - public void onListenerConnected(String[] notificationKeys) { - try { - NotificationListenerService.this.onListenerConnected(notificationKeys); - } catch (Throwable t) { - Log.w(TAG, "Error running onListenerConnected", t); + public void onListenerConnected(NotificationRankingUpdate update) { + // protect subclass from concurrent modifications of (@link mNotificationKeys}. + synchronized (mWrapper) { + applyUpdate(update); + try { + NotificationListenerService.this.onListenerConnected(); + } catch (Throwable t) { + Log.w(TAG, "Error running onListenerConnected", t); + } } } + @Override + public void onNotificationRankingUpdate(NotificationRankingUpdate update) + throws RemoteException { + // protect subclass from concurrent modifications of (@link mNotificationKeys}. + synchronized (mWrapper) { + applyUpdate(update); + try { + NotificationListenerService.this.onNotificationRankingUpdate(); + } catch (Throwable t) { + Log.w(TAG, "Error running onNotificationRankingUpdate", t); + } + } + } + } + + private void applyUpdate(NotificationRankingUpdate update) { + mRanking = new Ranking(update); + } + + /** + * Provides access to ranking information on currently active + * notifications. + * + * <p> + * Note that this object represents a ranking snapshot that only applies to + * notifications active at the time of retrieval. + */ + public static class Ranking implements Parcelable { + private final NotificationRankingUpdate mRankingUpdate; + + private Ranking(NotificationRankingUpdate rankingUpdate) { + mRankingUpdate = rankingUpdate; + } + + /** + * Request the list of notification keys in their current ranking + * order. + * + * @return An array of active notification keys, in their ranking order. + */ + public String[] getOrderedKeys() { + return mRankingUpdate.getOrderedKeys(); + } + + /** + * Returns the rank of the notification with the given key, that is the + * index of <code>key</code> in the array of keys returned by + * {@link #getOrderedKeys()}. + * + * @return The rank of the notification with the given key; -1 when the + * given key is unknown. + */ + public int getRank(String key) { + // TODO: Optimize. + String[] orderedKeys = mRankingUpdate.getOrderedKeys(); + for (int i = 0; i < orderedKeys.length; i++) { + if (orderedKeys[i].equals(key)) { + return i; + } + } + return -1; + } + + /** + * Returns whether the notification with the given key was intercepted + * by "Do not disturb". + */ + public boolean isInterceptedByDoNotDisturb(String key) { + // TODO: Optimize. + for (String interceptedKey : mRankingUpdate.getDndInterceptedKeys()) { + if (interceptedKey.equals(key)) { + return true; + } + } + return false; + } + + /** + * Returns whether the notification with the given key is an ambient + * notification, that is a notification that doesn't require the user's + * immediate attention. + */ + public boolean isAmbient(String key) { + // TODO: Optimize. + int firstAmbientIndex = mRankingUpdate.getFirstAmbientIndex(); + if (firstAmbientIndex < 0) { + return false; + } + String[] orderedKeys = mRankingUpdate.getOrderedKeys(); + for (int i = firstAmbientIndex; i < orderedKeys.length; i++) { + if (orderedKeys[i].equals(key)) { + return true; + } + } + return false; + } + + // ----------- Parcelable + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(mRankingUpdate, flags); + } + + public static final Creator<Ranking> CREATOR = new Creator<Ranking>() { + @Override + public Ranking createFromParcel(Parcel source) { + NotificationRankingUpdate rankingUpdate = source.readParcelable(null); + return new Ranking(rankingUpdate); + } + + @Override + public Ranking[] newArray(int size) { + return new Ranking[size]; + } + }; } } diff --git a/core/java/android/service/notification/NotificationRankingUpdate.aidl b/core/java/android/service/notification/NotificationRankingUpdate.aidl new file mode 100644 index 0000000..1393cb9 --- /dev/null +++ b/core/java/android/service/notification/NotificationRankingUpdate.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.service.notification; + +parcelable NotificationRankingUpdate; diff --git a/core/java/android/service/notification/NotificationRankingUpdate.java b/core/java/android/service/notification/NotificationRankingUpdate.java new file mode 100644 index 0000000..4b13d95 --- /dev/null +++ b/core/java/android/service/notification/NotificationRankingUpdate.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.notification; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * @hide + */ +public class NotificationRankingUpdate implements Parcelable { + // TODO: Support incremental updates. + private final String[] mKeys; + private final String[] mDndInterceptedKeys; + private final int mFirstAmbientIndex; + + public NotificationRankingUpdate(String[] keys, String[] dndInterceptedKeys, + int firstAmbientIndex) { + mKeys = keys; + mFirstAmbientIndex = firstAmbientIndex; + mDndInterceptedKeys = dndInterceptedKeys; + } + + public NotificationRankingUpdate(Parcel in) { + mKeys = in.readStringArray(); + mFirstAmbientIndex = in.readInt(); + mDndInterceptedKeys = in.readStringArray(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeStringArray(mKeys); + out.writeInt(mFirstAmbientIndex); + out.writeStringArray(mDndInterceptedKeys); + } + + public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR + = new Parcelable.Creator<NotificationRankingUpdate>() { + public NotificationRankingUpdate createFromParcel(Parcel parcel) { + return new NotificationRankingUpdate(parcel); + } + + public NotificationRankingUpdate[] newArray(int size) { + return new NotificationRankingUpdate[size]; + } + }; + + public String[] getOrderedKeys() { + return mKeys; + } + + public int getFirstAmbientIndex() { + return mFirstAmbientIndex; + } + + public String[] getDndInterceptedKeys() { + return mDndInterceptedKeys; + } +} diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 846e292..d02fc7b 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -41,12 +41,18 @@ public class ZenModeConfig implements Parcelable { public static final String SLEEP_MODE_NIGHTS = "nights"; public static final String SLEEP_MODE_WEEKNIGHTS = "weeknights"; + public static final int SOURCE_ANYONE = 0; + public static final int SOURCE_CONTACT = 1; + public static final int SOURCE_STAR = 2; + public static final int MAX_SOURCE = SOURCE_STAR; + 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 ALLOW_ATT_FROM = "from"; private static final String SLEEP_TAG = "sleep"; private static final String SLEEP_ATT_MODE = "mode"; @@ -61,6 +67,7 @@ public class ZenModeConfig implements Parcelable { public boolean allowCalls; public boolean allowMessages; + public int allowFrom = SOURCE_ANYONE; public String sleepMode; public int sleepStartHour; @@ -92,6 +99,7 @@ public class ZenModeConfig implements Parcelable { conditionIds = new Uri[len]; source.readTypedArray(conditionIds, Uri.CREATOR); } + allowFrom = source.readInt(); } @Override @@ -120,6 +128,7 @@ public class ZenModeConfig implements Parcelable { } else { dest.writeInt(0); } + dest.writeInt(allowFrom); } @Override @@ -127,6 +136,7 @@ public class ZenModeConfig implements Parcelable { return new StringBuilder(ZenModeConfig.class.getSimpleName()).append('[') .append("allowCalls=").append(allowCalls) .append(",allowMessages=").append(allowMessages) + .append(",allowFrom=").append(sourceToString(allowFrom)) .append(",sleepMode=").append(sleepMode) .append(",sleepStart=").append(sleepStartHour).append('.').append(sleepStartMinute) .append(",sleepEnd=").append(sleepEndHour).append('.').append(sleepEndMinute) @@ -137,6 +147,19 @@ public class ZenModeConfig implements Parcelable { .append(']').toString(); } + public static String sourceToString(int source) { + switch (source) { + case SOURCE_ANYONE: + return "anyone"; + case SOURCE_CONTACT: + return "contacts"; + case SOURCE_STAR: + return "stars"; + default: + return "UNKNOWN"; + } + } + @Override public boolean equals(Object o) { if (!(o instanceof ZenModeConfig)) return false; @@ -144,6 +167,7 @@ public class ZenModeConfig implements Parcelable { final ZenModeConfig other = (ZenModeConfig) o; return other.allowCalls == allowCalls && other.allowMessages == allowMessages + && other.allowFrom == allowFrom && Objects.equals(other.sleepMode, sleepMode) && other.sleepStartHour == sleepStartHour && other.sleepStartMinute == sleepStartMinute @@ -155,8 +179,8 @@ public class ZenModeConfig implements Parcelable { @Override public int hashCode() { - return Objects.hash(allowCalls, allowMessages, sleepMode, sleepStartHour, - sleepStartMinute, sleepEndHour, sleepEndMinute, + return Objects.hash(allowCalls, allowMessages, allowFrom, sleepMode, + sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute, Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds)); } @@ -191,6 +215,10 @@ public class ZenModeConfig implements Parcelable { if (ALLOW_TAG.equals(tag)) { rt.allowCalls = safeBoolean(parser, ALLOW_ATT_CALLS, false); rt.allowMessages = safeBoolean(parser, ALLOW_ATT_MESSAGES, false); + rt.allowFrom = safeInt(parser, ALLOW_ATT_FROM, SOURCE_ANYONE); + if (rt.allowFrom < SOURCE_ANYONE || rt.allowFrom > MAX_SOURCE) { + throw new IndexOutOfBoundsException("bad source in config:" + rt.allowFrom); + } } else if (SLEEP_TAG.equals(tag)) { final String mode = parser.getAttributeValue(null, SLEEP_ATT_MODE); rt.sleepMode = (SLEEP_MODE_NIGHTS.equals(mode) @@ -224,6 +252,7 @@ public class ZenModeConfig implements Parcelable { out.startTag(null, ALLOW_TAG); out.attribute(null, ALLOW_ATT_CALLS, Boolean.toString(allowCalls)); out.attribute(null, ALLOW_ATT_MESSAGES, Boolean.toString(allowMessages)); + out.attribute(null, ALLOW_ATT_FROM, Integer.toString(allowFrom)); out.endTag(null, ALLOW_TAG); out.startTag(null, SLEEP_TAG); diff --git a/core/java/android/service/trust/ITrustAgentServiceCallback.aidl b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl index c346771..9e4c2bf 100644 --- a/core/java/android/service/trust/ITrustAgentServiceCallback.aidl +++ b/core/java/android/service/trust/ITrustAgentServiceCallback.aidl @@ -23,6 +23,6 @@ import android.os.UserHandle; * @hide */ oneway interface ITrustAgentServiceCallback { - void enableTrust(String message, long durationMs, boolean initiatedByUser); + void grantTrust(CharSequence 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 index d5ce429..98f70f4 100644 --- a/core/java/android/service/trust/TrustAgentService.java +++ b/core/java/android/service/trust/TrustAgentService.java @@ -16,12 +16,17 @@ package android.service.trust; +import android.Manifest; import android.annotation.SdkConstant; import android.app.Service; +import android.content.ComponentName; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ServiceInfo; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.util.Log; import android.util.Slog; /** @@ -29,12 +34,12 @@ import android.util.Slog; * 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 + * the {@link android.Manifest.permission#BIND_TRUST_AGENT} 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"> + * android:permission="android.permission.BIND_TRUST_AGENT"> * <intent-filter> * <action android:name="android.service.trust.TrustAgentService" /> * </intent-filter> @@ -47,7 +52,7 @@ import android.util.Slog; * {@link android.R.styleable#TrustAgent}. For example:</p> * * <pre> - * <trust_agent xmlns:android="http://schemas.android.com/apk/res/android" + * <trust-agent xmlns:android="http://schemas.android.com/apk/res/android" * android:settingsActivity=".TrustAgentSettings" /></pre> */ public class TrustAgentService extends Service { @@ -83,12 +88,28 @@ public class TrustAgentService extends Service { }; }; + @Override + public void onCreate() { + super.onCreate(); + ComponentName component = new ComponentName(this, getClass()); + try { + ServiceInfo serviceInfo = getPackageManager().getServiceInfo(component, 0 /* flags */); + if (!Manifest.permission.BIND_TRUST_AGENT.equals(serviceInfo.permission)) { + throw new IllegalStateException(component.flattenToShortString() + + " is not declared with the permission " + + "\"" + Manifest.permission.BIND_TRUST_AGENT + "\""); + } + } catch (PackageManager.NameNotFoundException e) { + Log.e(TAG, "Can't get ServiceInfo for " + component.toShortString()); + } + } + /** * Called when the user attempted to authenticate on the device. * * @param successful true if the attempt succeeded */ - protected void onUnlockAttempt(boolean successful) { + public void onUnlockAttempt(boolean successful) { } private void onError(String msg) { @@ -96,7 +117,7 @@ public class TrustAgentService extends Service { } /** - * Call to enable trust on the device. + * Call to grant 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 @@ -104,10 +125,10 @@ public class TrustAgentService extends Service { * @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) { + public final void grantTrust(CharSequence message, long durationMs, boolean initiatedByUser) { if (mCallback != null) { try { - mCallback.enableTrust(message, durationMs, initiatedByUser); + mCallback.grantTrust(message.toString(), durationMs, initiatedByUser); } catch (RemoteException e) { onError("calling enableTrust()"); } @@ -117,7 +138,7 @@ public class TrustAgentService extends Service { /** * Call to revoke trust on the device. */ - protected final void revokeTrust() { + public final void revokeTrust() { if (mCallback != null) { try { mCallback.revokeTrust(); diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java index a83544d..2e9077a 100644 --- a/core/java/android/service/voice/VoiceInteractionSession.java +++ b/core/java/android/service/voice/VoiceInteractionSession.java @@ -20,8 +20,8 @@ import android.app.Dialog; import android.app.Instrumentation; import android.content.Context; import android.content.Intent; -import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Rect; import android.graphics.Region; import android.inputmethodservice.SoftInputWindow; import android.os.Binder; @@ -32,6 +32,7 @@ import android.os.Message; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; +import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -46,9 +47,22 @@ import com.android.internal.app.IVoiceInteractorRequest; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import java.lang.ref.WeakReference; + import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +/** + * An active voice interaction session, providing a facility for the implementation + * to interact with the user in the voice interaction layer. This interface is no shown + * by default, but you can request that it be shown with {@link #showWindow()}, which + * will result in a later call to {@link #onCreateContentView()} in which the UI can be + * built + * + * <p>A voice interaction session can be self-contained, ultimately calling {@link #finish} + * when done. It can also initiate voice interactions with applications by calling + * {@link #startVoiceActivity}</p>. + */ public abstract class VoiceInteractionSession implements KeyEvent.Callback { static final String TAG = "VoiceInteractionSession"; static final boolean DEBUG = true; @@ -79,11 +93,14 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { final Insets mTmpInsets = new Insets(); final int[] mTmpLocation = new int[2]; + final WeakReference<VoiceInteractionSession> mWeakRef + = new WeakReference<VoiceInteractionSession>(this); + final IVoiceInteractor mInteractor = new IVoiceInteractor.Stub() { @Override public IVoiceInteractorRequest startConfirmation(String callingPackage, - IVoiceInteractorCallback callback, String prompt, Bundle extras) { - Request request = findRequest(callback, true); + IVoiceInteractorCallback callback, CharSequence prompt, Bundle extras) { + Request request = newRequest(callback); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_CONFIRMATION, new Caller(callingPackage, Binder.getCallingUid()), request, prompt, extras)); @@ -91,9 +108,19 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } @Override + public IVoiceInteractorRequest startAbortVoice(String callingPackage, + IVoiceInteractorCallback callback, CharSequence message, Bundle extras) { + Request request = newRequest(callback); + mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_ABORT_VOICE, + new Caller(callingPackage, Binder.getCallingUid()), request, + message, extras)); + return request.mInterface; + } + + @Override public IVoiceInteractorRequest startCommand(String callingPackage, IVoiceInteractorCallback callback, String command, Bundle extras) { - Request request = findRequest(callback, true); + Request request = newRequest(callback); mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageOOOO(MSG_START_COMMAND, new Caller(callingPackage, Binder.getCallingUid()), request, command, extras)); @@ -142,29 +169,60 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { final IVoiceInteractorRequest mInterface = new IVoiceInteractorRequest.Stub() { @Override public void cancel() throws RemoteException { - mHandlerCaller.sendMessage(mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this)); + VoiceInteractionSession session = mSession.get(); + if (session != null) { + session.mHandlerCaller.sendMessage( + session.mHandlerCaller.obtainMessageO(MSG_CANCEL, Request.this)); + } } }; final IVoiceInteractorCallback mCallback; - final HandlerCaller mHandlerCaller; - Request(IVoiceInteractorCallback callback, HandlerCaller handlerCaller) { + final WeakReference<VoiceInteractionSession> mSession; + + Request(IVoiceInteractorCallback callback, VoiceInteractionSession session) { mCallback = callback; - mHandlerCaller = handlerCaller; + mSession = session.mWeakRef; + } + + void finishRequest() { + VoiceInteractionSession session = mSession.get(); + if (session == null) { + throw new IllegalStateException("VoiceInteractionSession has been destroyed"); + } + Request req = session.removeRequest(mInterface.asBinder()); + if (req == null) { + throw new IllegalStateException("Request not active: " + this); + } else if (req != this) { + throw new IllegalStateException("Current active request " + req + + " not same as calling request " + this); + } } public void sendConfirmResult(boolean confirmed, Bundle result) { try { if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface + " confirmed=" + confirmed + " result=" + result); + finishRequest(); mCallback.deliverConfirmationResult(mInterface, confirmed, result); } catch (RemoteException e) { } } + public void sendAbortVoiceResult(Bundle result) { + try { + if (DEBUG) Log.d(TAG, "sendConfirmResult: req=" + mInterface + + " result=" + result); + finishRequest(); + mCallback.deliverAbortVoiceResult(mInterface, result); + } catch (RemoteException e) { + } + } + public void sendCommandResult(boolean complete, Bundle result) { try { if (DEBUG) Log.d(TAG, "sendCommandResult: req=" + mInterface + " result=" + result); + finishRequest(); mCallback.deliverCommandResult(mInterface, complete, result); } catch (RemoteException e) { } @@ -173,6 +231,7 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { public void sendCancelResult() { try { if (DEBUG) Log.d(TAG, "sendCancelResult: req=" + mInterface); + finishRequest(); mCallback.deliverCancel(mInterface); } catch (RemoteException e) { } @@ -190,9 +249,10 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } 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; + static final int MSG_START_ABORT_VOICE = 2; + static final int MSG_START_COMMAND = 3; + static final int MSG_SUPPORTS_COMMANDS = 4; + static final int MSG_CANCEL = 5; static final int MSG_TASK_STARTED = 100; static final int MSG_TASK_FINISHED = 101; @@ -208,9 +268,16 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { args = (SomeArgs)msg.obj; 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, + onConfirm((Caller)args.arg1, (Request)args.arg2, (CharSequence)args.arg3, (Bundle)args.arg4); break; + case MSG_START_ABORT_VOICE: + args = (SomeArgs)msg.obj; + if (DEBUG) Log.d(TAG, "onAbortVoice: req=" + ((Request) args.arg2).mInterface + + " message=" + args.arg3 + " extras=" + args.arg4); + onAbortVoice((Caller) args.arg1, (Request) args.arg2, (CharSequence) args.arg3, + (Bundle) args.arg4); + break; case MSG_START_COMMAND: args = (SomeArgs)msg.obj; if (DEBUG) Log.d(TAG, "onCommand: req=" + ((Request) args.arg2).mInterface @@ -262,14 +329,14 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { */ public static final class Insets { /** - * This is the top part of the UI that is the main content. It is + * This is the part of the UI that is the main content. It is * used to determine the basic space needed, to resize/pan the * application behind. It is assumed that this inset does not * change very much, since any change will cause a full resize/pan * of the application behind. This value is relative to the top edge * of the input method window. */ - public int contentTopInsets; + public final Rect contentInsets = new Rect(); /** * This is the region of the UI that is touchable. It is used when @@ -311,7 +378,8 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { new ViewTreeObserver.OnComputeInternalInsetsListener() { public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { onComputeInsets(mTmpInsets); - info.contentInsets.top = info.visibleInsets.top = mTmpInsets.contentTopInsets; + info.contentInsets.set(mTmpInsets.contentInsets); + info.visibleInsets.set(mTmpInsets.contentInsets); info.touchableRegion.set(mTmpInsets.touchableRegion); info.setTouchableInsets(mTmpInsets.touchableInsets); } @@ -327,18 +395,20 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { mCallbacks, true); } - Request findRequest(IVoiceInteractorCallback callback, boolean newRequest) { + Request newRequest(IVoiceInteractorCallback callback) { + synchronized (this) { + Request req = new Request(callback, this); + mActiveRequests.put(req.mInterface.asBinder(), req); + return req; + } + } + + Request removeRequest(IBinder reqInterface) { synchronized (this) { - Request req = mActiveRequests.get(callback.asBinder()); + Request req = mActiveRequests.get(reqInterface); if (req != null) { - if (newRequest) { - throw new IllegalArgumentException("Given request callback " + callback - + " is already active"); - } - return req; + mActiveRequests.remove(req); } - req = new Request(callback, mHandlerCaller); - mActiveRequests.put(callback.asBinder(), req); return req; } } @@ -423,11 +493,34 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { mTheme = theme; } + /** + * Ask that a new activity be started for voice interaction. This will create a + * new dedicated task in the activity manager for this voice interaction session; + * this means that {@link Intent#FLAG_ACTIVITY_NEW_TASK Intent.FLAG_ACTIVITY_NEW_TASK} + * will be set for you to make it a new task. + * + * <p>The newly started activity will be displayed to the user in a special way, as + * a layer under the voice interaction UI.</p> + * + * <p>As the voice activity runs, it can retrieve a {@link android.app.VoiceInteractor} + * through which it can perform voice interactions through your session. These requests + * for voice interactions will appear as callbacks on {@link #onGetSupportedCommands}, + * {@link #onConfirm}, {@link #onCommand}, and {@link #onCancel}. + * + * <p>You will receive a call to {@link #onTaskStarted} when the task starts up + * and {@link #onTaskFinished} when the last activity has finished. + * + * @param intent The Intent to start this voice interaction. The given Intent will + * always have {@link Intent#CATEGORY_VOICE Intent.CATEGORY_VOICE} added to it, since + * this is part of a voice interaction. + */ public void startVoiceActivity(Intent intent) { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); } try { + intent.migrateExtraStreamToClipData(); + intent.prepareToLeaveProcess(); int res = mSystemService.startVoiceActivity(mToken, intent, intent.resolveType(mContext.getContentResolver())); Instrumentation.checkStartActivityResult(res, intent); @@ -435,14 +528,23 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } + /** + * Convenience for inflating views. + */ public LayoutInflater getLayoutInflater() { return mInflater; } + /** + * Retrieve the window being used to show the session's UI. + */ public Dialog getWindow() { return mWindow; } + /** + * Finish the session. + */ public void finish() { if (mToken == null) { throw new IllegalStateException("Can't call before onCreate()"); @@ -454,22 +556,35 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { } } + /** + * Initiatize a new session. + * + * @param args The arguments that were supplied to + * {@link VoiceInteractionService#startSession VoiceInteractionService.startSession}. + */ public void onCreate(Bundle args) { mTheme = mTheme != 0 ? mTheme : com.android.internal.R.style.Theme_DeviceDefault_VoiceInteractionSession; mInflater = (LayoutInflater)mContext.getSystemService( Context.LAYOUT_INFLATER_SERVICE); mWindow = new SoftInputWindow(mContext, "VoiceInteractionSession", mTheme, - mCallbacks, this, mDispatcherState, true); + mCallbacks, this, mDispatcherState, + WindowManager.LayoutParams.TYPE_VOICE_INTERACTION, Gravity.TOP, true); mWindow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); initViews(); mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT); mWindow.setToken(mToken); } + /** + * Last callback to the session as it is being finished. + */ public void onDestroy() { } + /** + * Hook in which to create the session's UI. + */ public View onCreateContentView() { return null; } @@ -502,6 +617,11 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { finish(); } + /** + * Sessions automatically watch for requests that all system UI be closed (such as when + * the user presses HOME), which will appear here. The default implementation always + * calls {@link #finish}. + */ public void onCloseSystemDialogs() { finish(); } @@ -517,20 +637,106 @@ public abstract class VoiceInteractionSession implements KeyEvent.Callback { int[] loc = mTmpLocation; View decor = getWindow().getWindow().getDecorView(); decor.getLocationInWindow(loc); - outInsets.contentTopInsets = loc[1]; + outInsets.contentInsets.top = 0; + outInsets.contentInsets.left = 0; + outInsets.contentInsets.right = 0; + outInsets.contentInsets.bottom = 0; outInsets.touchableInsets = Insets.TOUCHABLE_INSETS_FRAME; outInsets.touchableRegion.setEmpty(); } + /** + * Called when a task initiated by {@link #startVoiceActivity(android.content.Intent)} + * has actually started. + * + * @param intent The original {@link Intent} supplied to + * {@link #startVoiceActivity(android.content.Intent)}. + * @param taskId Unique ID of the now running task. + */ public void onTaskStarted(Intent intent, int taskId) { } + /** + * Called when the last activity of a task initiated by + * {@link #startVoiceActivity(android.content.Intent)} has finished. The default + * implementation calls {@link #finish()} on the assumption that this represents + * the completion of a voice action. You can override the implementation if you would + * like a different behavior. + * + * @param intent The original {@link Intent} supplied to + * {@link #startVoiceActivity(android.content.Intent)}. + * @param taskId Unique ID of the finished task. + */ public void onTaskFinished(Intent intent, int taskId) { finish(); } - public abstract boolean[] onGetSupportedCommands(Caller caller, String[] commands); - public abstract void onConfirm(Caller caller, Request request, String prompt, Bundle extras); + /** + * Request to query for what extended commands the session supports. + * + * @param caller Who is making the request. + * @param commands An array of commands that are being queried. + * @return Return an array of booleans indicating which of each entry in the + * command array is supported. A true entry in the array indicates the command + * is supported; false indicates it is not. The default implementation returns + * an array of all false entries. + */ + public boolean[] onGetSupportedCommands(Caller caller, String[] commands) { + return new boolean[commands.length]; + } + + /** + * Request to confirm with the user before proceeding with an unrecoverable operation, + * corresponding to a {@link android.app.VoiceInteractor.ConfirmationRequest + * VoiceInteractor.ConfirmationRequest}. + * + * @param caller Who is making the request. + * @param request The active request. + * @param prompt The prompt informing the user of what will happen, as per + * {@link android.app.VoiceInteractor.ConfirmationRequest VoiceInteractor.ConfirmationRequest}. + * @param extras Any additional information, as per + * {@link android.app.VoiceInteractor.ConfirmationRequest VoiceInteractor.ConfirmationRequest}. + */ + public abstract void onConfirm(Caller caller, Request request, CharSequence prompt, + Bundle extras); + + /** + * Request to abort the voice interaction session because the voice activity can not + * complete its interaction using voice. Corresponds to + * {@link android.app.VoiceInteractor.AbortVoiceRequest + * VoiceInteractor.AbortVoiceRequest}. The default implementation just sends an empty + * confirmation back to allow the activity to exit. + * + * @param caller Who is making the request. + * @param request The active request. + * @param message The message informing the user of the problem, as per + * {@link android.app.VoiceInteractor.AbortVoiceRequest VoiceInteractor.AbortVoiceRequest}. + * @param extras Any additional information, as per + * {@link android.app.VoiceInteractor.AbortVoiceRequest VoiceInteractor.AbortVoiceRequest}. + */ + public void onAbortVoice(Caller caller, Request request, CharSequence message, Bundle extras) { + request.sendAbortVoiceResult(null); + } + + /** + * Process an arbitrary extended command from the caller, + * corresponding to a {@link android.app.VoiceInteractor.CommandRequest + * VoiceInteractor.CommandRequest}. + * + * @param caller Who is making the request. + * @param request The active request. + * @param command The command that is being executed, as per + * {@link android.app.VoiceInteractor.CommandRequest VoiceInteractor.CommandRequest}. + * @param extras Any additional information, as per + * {@link android.app.VoiceInteractor.CommandRequest VoiceInteractor.CommandRequest}. + */ public abstract void onCommand(Caller caller, Request request, String command, Bundle extras); + + /** + * Called when the {@link android.app.VoiceInteractor} has asked to cancel a {@link Request} + * that was previously delivered to {@link #onConfirm} or {@link #onCommand}. + * + * @param request The request that is being canceled. + */ public abstract void onCancel(Request request); } diff --git a/core/java/android/speech/tts/Markup.java b/core/java/android/speech/tts/Markup.java new file mode 100644 index 0000000..c886e5d --- /dev/null +++ b/core/java/android/speech/tts/Markup.java @@ -0,0 +1,537 @@ +package android.speech.tts; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class that provides markup to a synthesis request to control aspects of speech. + * <p> + * Markup itself is a feature agnostic data format; the {@link Utterance} class defines the currently + * available set of features and should be used to construct instances of the Markup class. + * </p> + * <p> + * A marked up sentence is a tree. Each node has a type, an optional plain text, a set of + * parameters, and a list of children. + * The <b>type</b> defines what it contains, e.g. "text", "date", "measure", etc. A Markup node + * can be either a part of sentence (often a leaf node), or node altering some property of its + * children (node with children). The top level node has to be of type "utterance" and its children + * are synthesized in order. + * The <b>plain text</b> is optional except for the top level node. If the synthesis engine does not + * support Markup at all, it should use the plain text of the top level node. If an engine does not + * recognize or support a node type, it will try to use the plain text of that node if provided. If + * the plain text is null, it will synthesize its children in order. + * <b>Parameters</b> are key-value pairs specific to each node type. In case of a date node the + * parameters may be for example "month: 7" and "day: 10". + * The <b>nested markups</b> are children and can for example be used to nest semiotic classes (a + * measure may have a node of type "decimal" as its child) or to modify some property of its + * children. See "plain text" on how they are processed if the parent of the children is unknown to + * the engine. + * <p> + */ +public final class Markup implements Parcelable { + + private String mType; + private String mPlainText; + + private Bundle mParameters = new Bundle(); + private List<Markup> mNestedMarkups = new ArrayList<Markup>(); + + private static final String TYPE = "type"; + private static final String PLAIN_TEXT = "plain_text"; + private static final String MARKUP = "markup"; + + private static final String IDENTIFIER_REGEX = "([0-9a-z_]+)"; + private static final Pattern legalIdentifierPattern = Pattern.compile(IDENTIFIER_REGEX); + + /** + * Constructs an empty markup. + */ + public Markup() {} + + /** + * Constructs a markup of the given type. + */ + public Markup(String type) { + setType(type); + } + + /** + * Returns the type of this node; can be null. + */ + public String getType() { + return mType; + } + + /** + * Sets the type of this node. can be null. May only contain [0-9a-z_]. + */ + public void setType(String type) { + if (type != null) { + Matcher matcher = legalIdentifierPattern.matcher(type); + if (!matcher.matches()) { + throw new IllegalArgumentException("Type cannot be empty and may only contain " + + "0-9, a-z and underscores."); + } + } + mType = type; + } + + /** + * Returns this node's plain text; can be null. + */ + public String getPlainText() { + return mPlainText; + } + + /** + * Sets this nodes's plain text; can be null. + */ + public void setPlainText(String plainText) { + mPlainText = plainText; + } + + /** + * Adds or modifies a parameter. + * @param key The key; may only contain [0-9a-z_] and cannot be "type" or "plain_text". + * @param value The value. + * @throws An {@link IllegalArgumentException} if the key is null or empty. + * @return this + */ + public Markup setParameter(String key, String value) { + if (key == null || key.isEmpty()) { + throw new IllegalArgumentException("Key cannot be null or empty."); + } + if (key.equals("type")) { + throw new IllegalArgumentException("Key cannot be \"type\"."); + } + if (key.equals("plain_text")) { + throw new IllegalArgumentException("Key cannot be \"plain_text\"."); + } + Matcher matcher = legalIdentifierPattern.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException("Key may only contain 0-9, a-z and underscores."); + } + + if (value != null) { + mParameters.putString(key, value); + } else { + removeParameter(key); + } + return this; + } + + /** + * Removes the parameter with the given key + */ + public void removeParameter(String key) { + mParameters.remove(key); + } + + /** + * Returns the value of the parameter. + * @param key The parameter key. + * @return The value of the parameter or null if the parameter is not set. + */ + public String getParameter(String key) { + return mParameters.getString(key); + } + + /** + * Returns the number of parameters that have been set. + */ + public int parametersSize() { + return mParameters.size(); + } + + /** + * Appends a child to the list of children + * @param markup The child. + * @return This instance. + * @throws {@link IllegalArgumentException} if markup is null. + */ + public Markup addNestedMarkup(Markup markup) { + if (markup == null) { + throw new IllegalArgumentException("Nested markup cannot be null"); + } + mNestedMarkups.add(markup); + return this; + } + + /** + * Removes the given node from its children. + * @param markup The child to remove. + * @return True if this instance was modified by this operation, false otherwise. + */ + public boolean removeNestedMarkup(Markup markup) { + return mNestedMarkups.remove(markup); + } + + /** + * Returns the index'th child. + * @param i The index of the child. + * @return The child. + * @throws {@link IndexOutOfBoundsException} if i < 0 or i >= nestedMarkupSize() + */ + public Markup getNestedMarkup(int i) { + return mNestedMarkups.get(i); + } + + + /** + * Returns the number of children. + */ + public int nestedMarkupSize() { + return mNestedMarkups.size(); + } + + /** + * Returns a string representation of this Markup instance. Can be deserialized back to a Markup + * instance with markupFromString(). + */ + public String toString() { + StringBuilder out = new StringBuilder(); + if (mType != null) { + out.append(TYPE + ": \"" + mType + "\""); + } + if (mPlainText != null) { + out.append(out.length() > 0 ? " " : ""); + out.append(PLAIN_TEXT + ": \"" + escapeQuotedString(mPlainText) + "\""); + } + // Sort the parameters alphabetically by key so we have a stable output. + SortedMap<String, String> sortedMap = new TreeMap<String, String>(); + for (String key : mParameters.keySet()) { + sortedMap.put(key, mParameters.getString(key)); + } + for (Map.Entry<String, String> entry : sortedMap.entrySet()) { + out.append(out.length() > 0 ? " " : ""); + out.append(entry.getKey() + ": \"" + escapeQuotedString(entry.getValue()) + "\""); + } + for (Markup m : mNestedMarkups) { + out.append(out.length() > 0 ? " " : ""); + String nestedStr = m.toString(); + if (nestedStr.isEmpty()) { + out.append(MARKUP + " {}"); + } else { + out.append(MARKUP + " { " + m.toString() + " }"); + } + } + return out.toString(); + } + + /** + * Escapes backslashes and double quotes in the plain text and parameter values before this + * instance is written to a string. + * @param str The string to escape. + * @return The escaped string. + */ + private static String escapeQuotedString(String str) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '"') { + out.append("\\\""); + } else if (str.charAt(i) == '\\') { + out.append("\\\\"); + } else { + out.append(c); + } + } + return out.toString(); + } + + /** + * The reverse of the escape method, returning plain text and parameter values to their original + * form. + * @param str An escaped string. + * @return The unescaped string. + */ + private static String unescapeQuotedString(String str) { + StringBuilder out = new StringBuilder(); + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == '\\') { + i++; + if (i >= str.length()) { + throw new IllegalArgumentException("Unterminated escape sequence in string: " + + str); + } + c = str.charAt(i); + if (c == '\\') { + out.append("\\"); + } else if (c == '"') { + out.append("\""); + } else { + throw new IllegalArgumentException("Unsupported escape sequence: \\" + c + + " in string " + str); + } + } else { + out.append(c); + } + } + return out.toString(); + } + + /** + * Returns true if the given string consists only of whitespace. + * @param str The string to check. + * @return True if the given string consists only of whitespace. + */ + private static boolean isWhitespace(String str) { + return Pattern.matches("\\s*", str); + } + + /** + * Parses the given string, and overrides the values of this instance with those contained + * in the given string. + * @param str The string to parse; can have superfluous whitespace. + * @return An empty string on success, else the remainder of the string that could not be + * parsed. + */ + private String fromReadableString(String str) { + while (!isWhitespace(str)) { + String newStr = matchValue(str); + if (newStr == null) { + newStr = matchMarkup(str); + + if (newStr == null) { + return str; + } + } + str = newStr; + } + return ""; + } + + // Matches: key : "value" + // where key is an identifier and value can contain escaped quotes + // there may be superflouous whitespace + // The value string may contain quotes and backslashes. + private static final String OPTIONAL_WHITESPACE = "\\s*"; + private static final String VALUE_REGEX = "((\\\\.|[^\\\"])*)"; + private static final String KEY_VALUE_REGEX = + "\\A" + OPTIONAL_WHITESPACE + // start of string + IDENTIFIER_REGEX + OPTIONAL_WHITESPACE + ":" + OPTIONAL_WHITESPACE + // key: + "\"" + VALUE_REGEX + "\""; // "value" + private static final Pattern KEY_VALUE_PATTERN = Pattern.compile(KEY_VALUE_REGEX); + + /** + * Tries to match a key-value pair at the start of the string. If found, add that as a parameter + * of this instance. + * @param str The string to parse. + * @return The remainder of the string without the parsed key-value pair on success, else null. + */ + private String matchValue(String str) { + // Matches: key: "value" + Matcher matcher = KEY_VALUE_PATTERN.matcher(str); + if (!matcher.find()) { + return null; + } + String key = matcher.group(1); + String value = matcher.group(2); + + if (key == null || value == null) { + return null; + } + String unescapedValue = unescapeQuotedString(value); + if (key.equals(TYPE)) { + this.mType = unescapedValue; + } else if (key.equals(PLAIN_TEXT)) { + this.mPlainText = unescapedValue; + } else { + setParameter(key, unescapedValue); + } + + return str.substring(matcher.group(0).length()); + } + + // matches 'markup {' + private static final Pattern OPEN_MARKUP_PATTERN = + Pattern.compile("\\A" + OPTIONAL_WHITESPACE + MARKUP + OPTIONAL_WHITESPACE + "\\{"); + // matches '}' + private static final Pattern CLOSE_MARKUP_PATTERN = + Pattern.compile("\\A" + OPTIONAL_WHITESPACE + "\\}"); + + /** + * Tries to parse a Markup specification from the start of the string. If so, add that markup to + * the list of nested Markup's of this instance. + * @param str The string to parse. + * @return The remainder of the string without the parsed Markup on success, else null. + */ + private String matchMarkup(String str) { + // find and strip "markup {" + Matcher matcher = OPEN_MARKUP_PATTERN.matcher(str); + + if (!matcher.find()) { + return null; + } + String strRemainder = str.substring(matcher.group(0).length()); + // parse and strip markup contents + Markup nestedMarkup = new Markup(); + strRemainder = nestedMarkup.fromReadableString(strRemainder); + + // find and strip "}" + Matcher matcherClose = CLOSE_MARKUP_PATTERN.matcher(strRemainder); + if (!matcherClose.find()) { + return null; + } + strRemainder = strRemainder.substring(matcherClose.group(0).length()); + + // Everything parsed, add markup + this.addNestedMarkup(nestedMarkup); + + // Return remainder + return strRemainder; + } + + /** + * Returns a Markup instance from the string representation generated by toString(). + * @param string The string representation generated by toString(). + * @return The new Markup instance. + * @throws {@link IllegalArgumentException} if the input cannot be correctly parsed. + */ + public static Markup markupFromString(String string) throws IllegalArgumentException { + Markup m = new Markup(); + if (m.fromReadableString(string).isEmpty()) { + return m; + } else { + throw new IllegalArgumentException("Cannot parse input to Markup"); + } + } + + /** + * Compares the specified object with this Markup for equality. + * @return True if the given object is a Markup instance with the same type, plain text, + * parameters and the nested markups are also equal to each other and in the same order. + */ + @Override + public boolean equals(Object o) { + if ( this == o ) return true; + if ( !(o instanceof Markup) ) return false; + Markup m = (Markup) o; + + if (nestedMarkupSize() != this.nestedMarkupSize()) { + return false; + } + + if (!(mType == null ? m.mType == null : mType.equals(m.mType))) { + return false; + } + if (!(mPlainText == null ? m.mPlainText == null : mPlainText.equals(m.mPlainText))) { + return false; + } + if (!equalBundles(mParameters, m.mParameters)) { + return false; + } + + for (int i = 0; i < this.nestedMarkupSize(); i++) { + if (!mNestedMarkups.get(i).equals(m.mNestedMarkups.get(i))) { + return false; + } + } + + return true; + } + + /** + * Checks if two bundles are equal to each other. Used by equals(o). + */ + private boolean equalBundles(Bundle one, Bundle two) { + if (one == null || two == null) { + return false; + } + + if(one.size() != two.size()) { + return false; + } + + Set<String> valuesOne = one.keySet(); + for(String key : valuesOne) { + Object valueOne = one.get(key); + Object valueTwo = two.get(key); + if (valueOne instanceof Bundle && valueTwo instanceof Bundle && + !equalBundles((Bundle) valueOne, (Bundle) valueTwo)) { + return false; + } else if (valueOne == null) { + if (valueTwo != null || !two.containsKey(key)) { + return false; + } + } else if(!valueOne.equals(valueTwo)) { + return false; + } + } + return true; + } + + /** + * Returns an unmodifiable list of the children. + * @return An unmodifiable list of children that throws an {@link UnsupportedOperationException} + * if an attempt is made to modify it + */ + public List<Markup> getNestedMarkups() { + return Collections.unmodifiableList(mNestedMarkups); + } + + /** + * @hide + */ + public Markup(Parcel in) { + mType = in.readString(); + mPlainText = in.readString(); + mParameters = in.readBundle(); + in.readList(mNestedMarkups, Markup.class.getClassLoader()); + } + + /** + * Creates a deep copy of the given markup. + */ + public Markup(Markup markup) { + mType = markup.mType; + mPlainText = markup.mPlainText; + mParameters = markup.mParameters; + for (Markup nested : markup.getNestedMarkups()) { + addNestedMarkup(new Markup(nested)); + } + } + + /** + * @hide + */ + public int describeContents() { + return 0; + } + + /** + * @hide + */ + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mType); + dest.writeString(mPlainText); + dest.writeBundle(mParameters); + dest.writeList(mNestedMarkups); + } + + /** + * @hide + */ + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public Markup createFromParcel(Parcel in) { + return new Markup(in); + } + + public Markup[] newArray(int size) { + return new Markup[size]; + } + }; +} + diff --git a/core/java/android/speech/tts/SynthesisRequestV2.java b/core/java/android/speech/tts/SynthesisRequestV2.java index a1da49c..130e3f9 100644 --- a/core/java/android/speech/tts/SynthesisRequestV2.java +++ b/core/java/android/speech/tts/SynthesisRequestV2.java @@ -4,11 +4,12 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.speech.tts.TextToSpeechClient.UtteranceId; +import android.util.Log; /** * Service-side representation of a synthesis request from a V2 API client. Contains: * <ul> - * <li>The utterance to synthesize</li> + * <li>The markup object to synthesize containing the utterance.</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> @@ -16,8 +17,11 @@ import android.speech.tts.TextToSpeechClient.UtteranceId; * </ul> */ public final class SynthesisRequestV2 implements Parcelable { - /** Synthesis utterance. */ - private final String mText; + + private static final String TAG = "SynthesisRequestV2"; + + /** Synthesis markup */ + private final Markup mMarkup; /** Synthesis id. */ private final String mUtteranceId; @@ -34,9 +38,9 @@ public final class SynthesisRequestV2 implements Parcelable { /** * Constructor for test purposes. */ - public SynthesisRequestV2(String text, String utteranceId, String voiceName, + public SynthesisRequestV2(Markup markup, String utteranceId, String voiceName, Bundle voiceParams, Bundle audioParams) { - this.mText = text; + this.mMarkup = markup; this.mUtteranceId = utteranceId; this.mVoiceName = voiceName; this.mVoiceParams = voiceParams; @@ -49,15 +53,18 @@ public final class SynthesisRequestV2 implements Parcelable { * @hide */ public SynthesisRequestV2(Parcel in) { - this.mText = in.readString(); + this.mMarkup = (Markup) in.readValue(Markup.class.getClassLoader()); 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; + /** + * Constructor to request the synthesis of a sentence. + */ + SynthesisRequestV2(Markup markup, String utteranceId, RequestConfig rconfig) { + this.mMarkup = markup; this.mUtteranceId = utteranceId; this.mVoiceName = rconfig.getVoice().getName(); this.mVoiceParams = rconfig.getVoiceParams(); @@ -71,7 +78,7 @@ public final class SynthesisRequestV2 implements Parcelable { */ @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mText); + dest.writeValue(mMarkup); dest.writeString(mUtteranceId); dest.writeString(mVoiceName); dest.writeBundle(mVoiceParams); @@ -82,7 +89,18 @@ public final class SynthesisRequestV2 implements Parcelable { * @return the text which should be synthesized. */ public String getText() { - return mText; + if (mMarkup.getPlainText() == null) { + Log.e(TAG, "Plaintext of markup is null."); + return ""; + } + return mMarkup.getPlainText(); + } + + /** + * @return the markup which should be synthesized. + */ + public Markup getMarkup() { + return mMarkup; } /** diff --git a/core/java/android/speech/tts/TextToSpeechClient.java b/core/java/android/speech/tts/TextToSpeechClient.java index 10e2073..0c0be83 100644 --- a/core/java/android/speech/tts/TextToSpeechClient.java +++ b/core/java/android/speech/tts/TextToSpeechClient.java @@ -25,7 +25,10 @@ import android.content.ServiceConnection; import android.media.AudioManager; import android.net.Uri; import android.os.AsyncTask; +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.speech.tts.ITextToSpeechCallback; @@ -86,6 +89,8 @@ public class TextToSpeechClient { private HashMap<String, Pair<UtteranceId, RequestCallbacks>> mCallbacks; // Guarded by mLock + private InternalHandler mMainHandler = new InternalHandler(); + /** Common voices parameters */ public static final class Params { private Params() {} @@ -300,6 +305,8 @@ public class TextToSpeechClient { /** * Interface definition of callbacks that are called when the client is * connected or disconnected from the TTS service. + * + * The callbacks specified in this method will be called on the UI thread. */ public static interface ConnectionCallbacks { /** @@ -325,6 +332,9 @@ public class TextToSpeechClient { * 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. + * + * When the service is working again, the client will receive a callback to the + * {@link #onConnectionSuccess()} method. */ public void onServiceDisconnected(); @@ -502,7 +512,6 @@ public class TextToSpeechClient { } } - /** * Connects the client to TTS service. This method returns immediately, and connects to the * service in the background. @@ -688,7 +697,8 @@ public class TextToSpeechClient { synchronized (mLock) { mEngineStatus = new EngineStatus(mServiceConnection.getEngineName(), voicesInfo); - mConnectionCallbacks.onEngineStatusChange(mEngineStatus); + mMainHandler.obtainMessage(InternalHandler.WHAT_ENGINE_STATUS_CHANGED, + mEngineStatus).sendToTarget(); } } }; @@ -753,9 +763,11 @@ public class TextToSpeechClient { Log.i(TAG, "Asked to disconnect from " + name); synchronized(mLock) { + mEstablished = false; + mService = null; stopSetupConnectionTask(); } - mConnectionCallbacks.onServiceDisconnected(); + mMainHandler.obtainMessage(InternalHandler.WHAT_SERVICE_DISCONNECTED).sendToTarget(); } private void startSetupConnectionTask(ComponentName name) { @@ -781,15 +793,14 @@ public class TextToSpeechClient { return mService != null && mEstablished; } - boolean runAction(Action action) { + <T> ActionResult<T> runAction(Action<T> action) { synchronized (mLock) { try { - action.run(mService); - return true; + return new ActionResult<T>(true, action.run(mService)); } catch (Exception ex) { Log.e(TAG, action.getName() + " failed", ex); disconnect(); - return false; + return new ActionResult<T>(false); } } } @@ -809,7 +820,7 @@ public class TextToSpeechClient { } } - private abstract class Action { + private abstract class Action<T> { private final String mName; public Action(String name) { @@ -817,7 +828,21 @@ public class TextToSpeechClient { } public String getName() {return mName;} - abstract void run(ITextToSpeechService service) throws RemoteException; + abstract T run(ITextToSpeechService service) throws RemoteException; + } + + private class ActionResult<T> { + boolean mSuccess; + T mResult; + + ActionResult(boolean success) { + mSuccess = success; + } + + ActionResult(boolean success, T result) { + mSuccess = success; + mResult = result; + } } private IBinder getCallerIdentity() { @@ -827,16 +852,17 @@ public class TextToSpeechClient { return null; } - private boolean runAction(Action action) { + private <T> ActionResult<T> runAction(Action<T> action) { synchronized (mLock) { if (mServiceConnection == null) { - return false; + Log.w(TAG, action.getName() + " failed: not bound to TTS engine"); + return new ActionResult<T>(false); } if (!mServiceConnection.isEstablished()) { - return false; + Log.w(TAG, action.getName() + " failed: not fully bound to TTS engine"); + return new ActionResult<T>(false); } - mServiceConnection.runAction(action); - return true; + return mServiceConnection.runAction(action); } } @@ -847,13 +873,14 @@ public class TextToSpeechClient { * other utterances in the queue. */ public void stop() { - runAction(new Action(ACTION_STOP_NAME) { + runAction(new Action<Void>(ACTION_STOP_NAME) { @Override - public void run(ITextToSpeechService service) throws RemoteException { + public Void run(ITextToSpeechService service) throws RemoteException { if (service.stop(getCallerIdentity()) != Status.SUCCESS) { Log.e(TAG, "Stop failed"); } mCallbacks.clear(); + return null; } }); } @@ -861,7 +888,7 @@ public class TextToSpeechClient { private static final String ACTION_QUEUE_SPEAK_NAME = "queueSpeak"; /** - * Speaks the string using the specified queuing strategy using current + * Speaks the string using the specified queuing strategy and the 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. @@ -872,15 +899,38 @@ public class TextToSpeechClient { * 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 + * @param callbacks Synthesis request callbacks. If null, the 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) { + queueSpeak(createMarkupFromString(utterance), utteranceId, config, callbacks); + } + + /** + * Speaks the {@link Markup} (which can be constructed with {@link Utterance}) using + * the specified queuing strategy and the 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 markup The Markup to be spoken. The written equivalent of the spoken + * text should be 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, the default request + * callbacks object will be used. + */ + public void queueSpeak(final Markup markup, + final UtteranceId utteranceId, + final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action<Void>(ACTION_QUEUE_SPEAK_NAME) { @Override - public void run(ITextToSpeechService service) throws RemoteException { + public Void run(ITextToSpeechService service) throws RemoteException { RequestCallbacks c = mDefaultRequestCallbacks; if (callbacks != null) { c = callbacks; @@ -888,15 +938,16 @@ public class TextToSpeechClient { int addCallbackStatus = addCallback(utteranceId, c); if (addCallbackStatus != Status.SUCCESS) { c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); - return; + return null; } int queueResult = service.speakV2( getCallerIdentity(), - new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), config)); + new SynthesisRequestV2(markup, utteranceId.toUniqueString(), config)); if (queueResult != Status.SUCCESS) { removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); } + return null; } }); } @@ -916,15 +967,40 @@ public class TextToSpeechClient { * @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 + * @param callbacks Synthesis request callbacks. If null, the 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) { + queueSynthesizeToFile(createMarkupFromString(utterance), utteranceId, outputFile, config, callbacks); + } + + /** + * Synthesizes the given {@link Markup} (can be constructed with {@link Utterance}) + * 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 markup The Markup that should be synthesized. The written equivalent of + * the spoken text should be 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, the default request + * callbacks object will be used. + */ + public void queueSynthesizeToFile( + final Markup markup, + final UtteranceId utteranceId, + final File outputFile, final RequestConfig config, + final RequestCallbacks callbacks) { + runAction(new Action<Void>(ACTION_QUEUE_SYNTHESIZE_TO_FILE) { @Override - public void run(ITextToSpeechService service) throws RemoteException { + public Void run(ITextToSpeechService service) throws RemoteException { RequestCallbacks c = mDefaultRequestCallbacks; if (callbacks != null) { c = callbacks; @@ -932,7 +1008,7 @@ public class TextToSpeechClient { int addCallbackStatus = addCallback(utteranceId, c); if (addCallbackStatus != Status.SUCCESS) { c.onSynthesisFailure(utteranceId, Status.ERROR_INVALID_REQUEST); - return; + return null; } ParcelFileDescriptor fileDescriptor = null; @@ -940,7 +1016,7 @@ public class TextToSpeechClient { if (outputFile.exists() && !outputFile.canWrite()) { Log.e(TAG, "No permissions to write to " + outputFile); removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); - return; + return null; } fileDescriptor = ParcelFileDescriptor.open(outputFile, ParcelFileDescriptor.MODE_WRITE_ONLY | @@ -949,8 +1025,7 @@ public class TextToSpeechClient { int queueResult = service.synthesizeToFileDescriptorV2(getCallerIdentity(), fileDescriptor, - new SynthesisRequestV2(utterance, utteranceId.toUniqueString(), - config)); + new SynthesisRequestV2(markup, utteranceId.toUniqueString(), config)); fileDescriptor.close(); if (queueResult != Status.SUCCESS) { removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); @@ -962,10 +1037,18 @@ public class TextToSpeechClient { Log.e(TAG, "Closing file " + outputFile + " failed", e); removeCallbackAndErr(utteranceId.toUniqueString(), Status.ERROR_OUTPUT); } + return null; } }); } + private static Markup createMarkupFromString(String str) { + return new Utterance() + .append(new Utterance.TtsText(str)) + .setNoWarningOnFallback(true) + .createMarkup(); + } + private static final String ACTION_QUEUE_SILENCE_NAME = "queueSilence"; /** @@ -982,9 +1065,9 @@ public class TextToSpeechClient { */ public void queueSilence(final long durationInMs, final UtteranceId utteranceId, final RequestCallbacks callbacks) { - runAction(new Action(ACTION_QUEUE_SILENCE_NAME) { + runAction(new Action<Void>(ACTION_QUEUE_SILENCE_NAME) { @Override - public void run(ITextToSpeechService service) throws RemoteException { + public Void run(ITextToSpeechService service) throws RemoteException { RequestCallbacks c = mDefaultRequestCallbacks; if (callbacks != null) { c = callbacks; @@ -1000,6 +1083,7 @@ public class TextToSpeechClient { if (queueResult != Status.SUCCESS) { removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); } + return null; } }); } @@ -1023,9 +1107,9 @@ public class TextToSpeechClient { */ public void queueAudio(final Uri audioUrl, final UtteranceId utteranceId, final RequestConfig config, final RequestCallbacks callbacks) { - runAction(new Action(ACTION_QUEUE_AUDIO_NAME) { + runAction(new Action<Void>(ACTION_QUEUE_AUDIO_NAME) { @Override - public void run(ITextToSpeechService service) throws RemoteException { + public Void run(ITextToSpeechService service) throws RemoteException { RequestCallbacks c = mDefaultRequestCallbacks; if (callbacks != null) { c = callbacks; @@ -1041,7 +1125,49 @@ public class TextToSpeechClient { if (queueResult != Status.SUCCESS) { removeCallbackAndErr(utteranceId.toUniqueString(), queueResult); } + return null; } }); } + + private static final String ACTION_IS_SPEAKING_NAME = "isSpeaking"; + + /** + * Checks whether the TTS engine is busy speaking. Note that a speech item is + * considered complete once it's audio data has been sent to the audio mixer, or + * written to a file. There might be a finite lag between this point, and when + * the audio hardware completes playback. + * + * @return {@code true} if the TTS engine is speaking. + */ + public boolean isSpeaking() { + ActionResult<Boolean> result = runAction(new Action<Boolean>(ACTION_IS_SPEAKING_NAME) { + @Override + public Boolean run(ITextToSpeechService service) throws RemoteException { + return service.isSpeaking(); + } + }); + if (!result.mSuccess) { + return false; // We can't really say, return false + } + return result.mResult; + } + + + class InternalHandler extends Handler { + final static int WHAT_ENGINE_STATUS_CHANGED = 1; + final static int WHAT_SERVICE_DISCONNECTED = 2; + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case WHAT_ENGINE_STATUS_CHANGED: + mConnectionCallbacks.onEngineStatusChange((EngineStatus) msg.obj); + return; + case WHAT_SERVICE_DISCONNECTED: + mConnectionCallbacks.onServiceDisconnected(); + return; + } + } + } } diff --git a/core/java/android/speech/tts/TextToSpeechService.java b/core/java/android/speech/tts/TextToSpeechService.java index d7c51fc..14a4024 100644 --- a/core/java/android/speech/tts/TextToSpeechService.java +++ b/core/java/android/speech/tts/TextToSpeechService.java @@ -255,7 +255,8 @@ public abstract class TextToSpeechService extends Service { // 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); + // Speech speed <= 0 makes it use a system wide setting + defaultParams.putFloat(TextToSpeechClient.Params.SPEECH_SPEED, 0.0f); // Enumerate all locales and check if they are available ArrayList<VoiceInfo> voicesInfo = new ArrayList<VoiceInfo>(); @@ -351,6 +352,12 @@ public abstract class TextToSpeechService extends Service { params.putString(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true"); } + String noWarning = request.getMarkup().getParameter(Utterance.KEY_NO_WARNING_ON_FALLBACK); + if (noWarning == null || noWarning.equals("false")) { + Log.w("TextToSpeechService", "The synthesis engine does not support Markup, falling " + + "back to the given plain text."); + } + // Build V1 request SynthesisRequest requestV1 = new SynthesisRequest(request.getText(), params); Locale locale = selectedVoice.getLocale(); @@ -855,14 +862,53 @@ public abstract class TextToSpeechService extends Service { } } + /** + * Estimate of the character count equivalent of a Markup instance. Calculated + * by summing the characters of all Markups of type "text". Each other node + * is counted as a single character, as the character count of other nodes + * is non-trivial to calculate and we don't want to accept arbitrarily large + * requests. + */ + private int estimateSynthesisLengthFromMarkup(Markup m) { + int size = 0; + if (m.getType() != null && + m.getType().equals("text") && + m.getParameter("text") != null) { + size += m.getParameter("text").length(); + } else if (m.getType() == null || + !m.getType().equals("utterance")) { + size += 1; + } + for (Markup nested : m.getNestedMarkups()) { + size += estimateSynthesisLengthFromMarkup(nested); + } + return size; + } + @Override public boolean isValid() { - if (mSynthesisRequest.getText() == null) { - Log.e(TAG, "null synthesis text"); + if (mSynthesisRequest.getMarkup() == null) { + Log.e(TAG, "No markup in request."); return false; } - if (mSynthesisRequest.getText().length() >= TextToSpeech.getMaxSpeechInputLength()) { - Log.w(TAG, "Text too long: " + mSynthesisRequest.getText().length() + " chars"); + String type = mSynthesisRequest.getMarkup().getType(); + if (type == null) { + Log.w(TAG, "Top level markup node should have type \"utterance\", not null"); + return false; + } else if (!type.equals("utterance")) { + Log.w(TAG, "Top level markup node should have type \"utterance\" instead of " + + "\"" + type + "\""); + return false; + } + + int estimate = estimateSynthesisLengthFromMarkup(mSynthesisRequest.getMarkup()); + if (estimate >= TextToSpeech.getMaxSpeechInputLength()) { + Log.w(TAG, "Text too long: estimated size of text was " + estimate + " chars."); + return false; + } + + if (estimate <= 0) { + Log.e(TAG, "null synthesis text"); return false; } diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java index 4f996cd..9b929a3 100644 --- a/core/java/android/speech/tts/TtsEngines.java +++ b/core/java/android/speech/tts/TtsEngines.java @@ -307,6 +307,24 @@ public class TtsEngines { } /** + * True if a given TTS engine uses the default phone locale as a default locale. Attempts to + * read the value from {@link Settings.Secure#TTS_DEFAULT_LOCALE}, failing which the + * old style value from {@link Settings.Secure#TTS_DEFAULT_LANG} is read. If + * both these values are empty, this methods returns true. + * + * @param engineName the engine to return the locale for. + */ + public boolean isLocaleSetToDefaultForEngine(String engineName) { + return (TextUtils.isEmpty(parseEnginePrefFromList( + getString(mContext.getContentResolver(), Settings.Secure.TTS_DEFAULT_LOCALE), + engineName)) && + TextUtils.isEmpty( + Settings.Secure.getString(mContext.getContentResolver(), + Settings.Secure.TTS_DEFAULT_LANG))); + } + + + /** * Parses a locale preference value delimited by {@link #LOCALE_DELIMITER}. * Varies from {@link String#split} in that it will always return an array * of length 3 with non null values. diff --git a/core/java/android/speech/tts/Utterance.java b/core/java/android/speech/tts/Utterance.java new file mode 100644 index 0000000..0a29283 --- /dev/null +++ b/core/java/android/speech/tts/Utterance.java @@ -0,0 +1,595 @@ +package android.speech.tts; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class acts as a builder for {@link Markup} instances. + * <p> + * Each Utterance consists of a list of the semiotic classes ({@link Utterance.TtsCardinal} and + * {@link Utterance.TtsText}). + * <p>Each semiotic class can be supplied with morphosyntactic features + * (gender, animacy, multiplicity and case), it is up to the synthesis engine to use this + * information during synthesis. + * Examples where morphosyntactic features matter: + * <ul> + * <li>In French, the number one is verbalized differently based on the gender of the noun + * it modifies. "un homme" (one man) versus "une femme" (one woman). + * <li>In German the grammatical case (accusative, locative, etc) needs to be included to be + * verbalize correctly. In German you'd have the sentence "Sie haben 1 kilometer vor Ihnen" (You + * have 1 kilometer ahead of you), "1" in this case needs to become inflected to the accusative + * form ("einen") instead of the nominative form "ein". + * </p> + * <p> + * Utterance usage example: + * Markup m1 = new Utterance().append("The Eiffel Tower is") + * .append(new TtsCardinal(324)) + * .append("meters tall."); + * Markup m2 = new Utterance().append("Sie haben") + * .append(new TtsCardinal(1).setGender(Utterance.GENDER_MALE) + * .append("Tag frei."); + * </p> + */ +public class Utterance { + + /*** + * Toplevel type of markup representation. + */ + public static final String TYPE_UTTERANCE = "utterance"; + /*** + * The no_warning_on_fallback parameter can be set to "false" or "true", true indicating that + * no warning will be given when the synthesizer does not support Markup. This is used when + * the user only provides a string to the API instead of a markup. + */ + public static final String KEY_NO_WARNING_ON_FALLBACK = "no_warning_on_fallback"; + + // Gender. + public final static int GENDER_UNKNOWN = 0; + public final static int GENDER_NEUTRAL = 1; + public final static int GENDER_MALE = 2; + public final static int GENDER_FEMALE = 3; + + // Animacy. + public final static int ANIMACY_UNKNOWN = 0; + public final static int ANIMACY_ANIMATE = 1; + public final static int ANIMACY_INANIMATE = 2; + + // Multiplicity. + public final static int MULTIPLICITY_UNKNOWN = 0; + public final static int MULTIPLICITY_SINGLE = 1; + public final static int MULTIPLICITY_DUAL = 2; + public final static int MULTIPLICITY_PLURAL = 3; + + // Case. + public final static int CASE_UNKNOWN = 0; + public final static int CASE_NOMINATIVE = 1; + public final static int CASE_ACCUSATIVE = 2; + public final static int CASE_DATIVE = 3; + public final static int CASE_ABLATIVE = 4; + public final static int CASE_GENITIVE = 5; + public final static int CASE_VOCATIVE = 6; + public final static int CASE_LOCATIVE = 7; + public final static int CASE_INSTRUMENTAL = 8; + + private List<AbstractTts<? extends AbstractTts<?>>> says = + new ArrayList<AbstractTts<? extends AbstractTts<?>>>(); + Boolean mNoWarningOnFallback = null; + + /** + * Objects deriving from this class can be appended to a Utterance. This class uses generics + * so method from this class can return instances of its child classes, resulting in a better + * API (CRTP pattern). + */ + public static abstract class AbstractTts<C extends AbstractTts<C>> { + + protected Markup mMarkup = new Markup(); + + /** + * Empty constructor. + */ + protected AbstractTts() { + } + + /** + * Construct with Markup. + * @param markup + */ + protected AbstractTts(Markup markup) { + mMarkup = markup; + } + + /** + * Returns the type of this class, e.g. "cardinal" or "measure". + * @return The type. + */ + public String getType() { + return mMarkup.getType(); + } + + /** + * A fallback plain text can be provided, in case the engine does not support this class + * type, or even Markup altogether. + * @param plainText A string with the plain text. + * @return This instance. + */ + @SuppressWarnings("unchecked") + public C setPlainText(String plainText) { + mMarkup.setPlainText(plainText); + return (C) this; + } + + /** + * Returns the plain text (fallback) string. + * @return Plain text string or null if not set. + */ + public String getPlainText() { + return mMarkup.getPlainText(); + } + + /** + * Populates the plainText if not set and builds a Markup instance. + * @return The Markup object describing this instance. + */ + public Markup getMarkup() { + return new Markup(mMarkup); + } + + @SuppressWarnings("unchecked") + protected C setParameter(String key, String value) { + mMarkup.setParameter(key, value); + return (C) this; + } + + protected String getParameter(String key) { + return mMarkup.getParameter(key); + } + + @SuppressWarnings("unchecked") + protected C removeParameter(String key) { + mMarkup.removeParameter(key); + return (C) this; + } + + /** + * Returns a string representation of this instance, can be deserialized to an equal + * Utterance instance. + */ + public String toString() { + return mMarkup.toString(); + } + + /** + * Returns a generated plain text alternative for this instance if this instance isn't + * better representated by the list of it's children. + * @return Best effort plain text representation of this instance, can be null. + */ + public String generatePlainText() { + return null; + } + } + + public static abstract class AbstractTtsSemioticClass<C extends AbstractTtsSemioticClass<C>> + extends AbstractTts<C> { + // Keys. + private static final String KEY_GENDER = "gender"; + private static final String KEY_ANIMACY = "animacy"; + private static final String KEY_MULTIPLICITY = "multiplicity"; + private static final String KEY_CASE = "case"; + + protected AbstractTtsSemioticClass() { + super(); + } + + protected AbstractTtsSemioticClass(Markup markup) { + super(markup); + } + + @SuppressWarnings("unchecked") + public C setGender(int gender) { + if (gender < 0 || gender > 3) { + throw new IllegalArgumentException("Only four types of gender can be set: " + + "unknown, neutral, maculine and female."); + } + if (gender != GENDER_UNKNOWN) { + setParameter(KEY_GENDER, String.valueOf(gender)); + } else { + setParameter(KEY_GENDER, null); + } + return (C) this; + } + + public int getGender() { + String gender = mMarkup.getParameter(KEY_GENDER); + return gender != null ? Integer.valueOf(gender) : GENDER_UNKNOWN; + } + + @SuppressWarnings("unchecked") + public C setAnimacy(int animacy) { + if (animacy < 0 || animacy > 2) { + throw new IllegalArgumentException( + "Only two types of animacy can be set: unknown, animate and inanimate"); + } + if (animacy != ANIMACY_UNKNOWN) { + setParameter(KEY_ANIMACY, String.valueOf(animacy)); + } else { + setParameter(KEY_ANIMACY, null); + } + return (C) this; + } + + public int getAnimacy() { + String animacy = getParameter(KEY_ANIMACY); + return animacy != null ? Integer.valueOf(animacy) : ANIMACY_UNKNOWN; + } + + @SuppressWarnings("unchecked") + public C setMultiplicity(int multiplicity) { + if (multiplicity < 0 || multiplicity > 3) { + throw new IllegalArgumentException( + "Only four types of multiplicity can be set: unknown, single, dual and " + + "plural."); + } + if (multiplicity != MULTIPLICITY_UNKNOWN) { + setParameter(KEY_MULTIPLICITY, String.valueOf(multiplicity)); + } else { + setParameter(KEY_MULTIPLICITY, null); + } + return (C) this; + } + + public int getMultiplicity() { + String multiplicity = mMarkup.getParameter(KEY_MULTIPLICITY); + return multiplicity != null ? Integer.valueOf(multiplicity) : MULTIPLICITY_UNKNOWN; + } + + @SuppressWarnings("unchecked") + public C setCase(int grammaticalCase) { + if (grammaticalCase < 0 || grammaticalCase > 8) { + throw new IllegalArgumentException( + "Only nine types of grammatical case can be set."); + } + if (grammaticalCase != CASE_UNKNOWN) { + setParameter(KEY_CASE, String.valueOf(grammaticalCase)); + } else { + setParameter(KEY_CASE, null); + } + return (C) this; + } + + public int getCase() { + String grammaticalCase = mMarkup.getParameter(KEY_CASE); + return grammaticalCase != null ? Integer.valueOf(grammaticalCase) : CASE_UNKNOWN; + } + } + + /** + * Class that contains regular text, synthesis engine pronounces it using its regular pipeline. + * Parameters: + * <ul> + * <li>Text: the text to synthesize</li> + * </ul> + */ + public static class TtsText extends AbstractTtsSemioticClass<TtsText> { + + // The type of this node. + protected static final String TYPE_TEXT = "text"; + // The text parameter stores the text to be synthesized. + private static final String KEY_TEXT = "text"; + + /** + * Default constructor. + */ + public TtsText() { + mMarkup.setType(TYPE_TEXT); + } + + /** + * Constructor that sets the text to be synthesized. + * @param text The text to be synthesized. + */ + public TtsText(String text) { + this(); + setText(text); + } + + /** + * Constructs a TtsText with the values of the Markup, does not check if the given Markup is + * of the right type. + */ + private TtsText(Markup markup) { + super(markup); + } + + /** + * Sets the text to be synthesized. + * @return This instance. + */ + public TtsText setText(String text) { + setParameter(KEY_TEXT, text); + return this; + } + + /** + * Returns the text to be synthesized. + * @return This instance. + */ + public String getText() { + return getParameter(KEY_TEXT); + } + + /** + * Generates a best effort plain text, in this case simply the text. + */ + @Override + public String generatePlainText() { + return getText(); + } + } + + /** + * Contains a cardinal. + * Parameters: + * <ul> + * <li>integer: the integer to synthesize</li> + * </ul> + */ + public static class TtsCardinal extends AbstractTtsSemioticClass<TtsCardinal> { + + // The type of this node. + protected static final String TYPE_CARDINAL = "cardinal"; + // The parameter integer stores the integer to synthesize. + private static final String KEY_INTEGER = "integer"; + + /** + * Default constructor. + */ + public TtsCardinal() { + mMarkup.setType(TYPE_CARDINAL); + } + + /** + * Constructor that sets the integer to be synthesized. + */ + public TtsCardinal(int integer) { + this(); + setInteger(integer); + } + + /** + * Constructor that sets the integer to be synthesized. + */ + public TtsCardinal(String integer) { + this(); + setInteger(integer); + } + + /** + * Constructs a TtsText with the values of the Markup. + * Does not check if the given Markup is of the right type. + */ + private TtsCardinal(Markup markup) { + super(markup); + } + + /** + * Sets the integer. + * @return This instance. + */ + public TtsCardinal setInteger(int integer) { + return setInteger(String.valueOf(integer)); + } + + /** + * Sets the integer. + * @param integer A non-empty string of digits with an optional '-' in front. + * @return This instance. + */ + public TtsCardinal setInteger(String integer) { + if (!integer.matches("-?\\d+")) { + throw new IllegalArgumentException("Expected a cardinal: \"" + integer + "\""); + } + setParameter(KEY_INTEGER, integer); + return this; + } + + /** + * Returns the integer parameter. + */ + public String getInteger() { + return getParameter(KEY_INTEGER); + } + + /** + * Generates a best effort plain text, in this case simply the integer. + */ + @Override + public String generatePlainText() { + return getInteger(); + } + } + + /** + * Default constructor. + */ + public Utterance() {} + + /** + * Returns the plain text of a given Markup if it was set; if it's not set, recursively call the + * this same method on its children. + */ + private String constructPlainText(Markup m) { + StringBuilder plainText = new StringBuilder(); + if (m.getPlainText() != null) { + plainText.append(m.getPlainText()); + } else { + for (Markup nestedMarkup : m.getNestedMarkups()) { + String nestedPlainText = constructPlainText(nestedMarkup); + if (!nestedPlainText.isEmpty()) { + if (plainText.length() != 0) { + plainText.append(" "); + } + plainText.append(nestedPlainText); + } + } + } + return plainText.toString(); + } + + /** + * Creates a Markup instance with auto generated plain texts for the relevant nodes, in case the + * user has not provided one already. + * @return A Markup instance representing this utterance. + */ + public Markup createMarkup() { + Markup markup = new Markup(TYPE_UTTERANCE); + StringBuilder plainText = new StringBuilder(); + for (AbstractTts<? extends AbstractTts<?>> say : says) { + // Get a copy of this markup, and generate a plaintext for it if is not set. + Markup sayMarkup = say.getMarkup(); + if (sayMarkup.getPlainText() == null) { + sayMarkup.setPlainText(say.generatePlainText()); + } + if (plainText.length() != 0) { + plainText.append(" "); + } + plainText.append(constructPlainText(sayMarkup)); + markup.addNestedMarkup(sayMarkup); + } + if (mNoWarningOnFallback != null) { + markup.setParameter(KEY_NO_WARNING_ON_FALLBACK, + mNoWarningOnFallback ? "true" : "false"); + } + markup.setPlainText(plainText.toString()); + return markup; + } + + /** + * Appends an element to this Utterance instance. + * @return this instance + */ + public Utterance append(AbstractTts<? extends AbstractTts<?>> say) { + says.add(say); + return this; + } + + private Utterance append(Markup markup) { + if (markup.getType().equals(TtsText.TYPE_TEXT)) { + append(new TtsText(markup)); + } else if (markup.getType().equals(TtsCardinal.TYPE_CARDINAL)) { + append(new TtsCardinal(markup)); + } else { + // Unknown node, a class we don't know about. + if (markup.getPlainText() != null) { + append(new TtsText(markup.getPlainText())); + } else { + // No plainText specified; add its children + // seperately. In case of a new prosody node, + // we would still verbalize it correctly. + for (Markup nested : markup.getNestedMarkups()) { + append(nested); + } + } + } + return this; + } + + /** + * Returns a string representation of this Utterance instance. Can be deserialized back to an + * Utterance instance with utteranceFromString(). Can be used to store utterances to be used + * at a later time. + */ + public String toString() { + String out = "type: \"" + TYPE_UTTERANCE + "\""; + if (mNoWarningOnFallback != null) { + out += " no_warning_on_fallback: \"" + (mNoWarningOnFallback ? "true" : "false") + "\""; + } + for (AbstractTts<? extends AbstractTts<?>> say : says) { + out += " markup { " + say.getMarkup().toString() + " }"; + } + return out; + } + + /** + * Returns an Utterance instance from the string representation generated by toString(). + * @param string The string representation generated by toString(). + * @return The new Utterance instance. + * @throws {@link IllegalArgumentException} if the input cannot be correctly parsed. + */ + static public Utterance utteranceFromString(String string) throws IllegalArgumentException { + Utterance utterance = new Utterance(); + Markup markup = Markup.markupFromString(string); + if (!markup.getType().equals(TYPE_UTTERANCE)) { + throw new IllegalArgumentException("Top level markup should be of type \"" + + TYPE_UTTERANCE + "\", but was of type \"" + + markup.getType() + "\".") ; + } + for (Markup nestedMarkup : markup.getNestedMarkups()) { + utterance.append(nestedMarkup); + } + return utterance; + } + + /** + * Appends a new TtsText with the given text. + * @param text The text to synthesize. + * @return This instance. + */ + public Utterance append(String text) { + return append(new TtsText(text)); + } + + /** + * Appends a TtsCardinal representing the given number. + * @param integer The integer to synthesize. + * @return this + */ + public Utterance append(int integer) { + return append(new TtsCardinal(integer)); + } + + /** + * Returns the n'th element in this Utterance. + * @param i The index. + * @return The n'th element in this Utterance. + * @throws {@link IndexOutOfBoundsException} - if i < 0 || i >= size() + */ + public AbstractTts<? extends AbstractTts<?>> get(int i) { + return says.get(i); + } + + /** + * Returns the number of elements in this Utterance. + * @return The number of elements in this Utterance. + */ + public int size() { + return says.size(); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) return true; + if ( !(o instanceof Utterance) ) return false; + Utterance utt = (Utterance) o; + + if (says.size() != utt.says.size()) { + return false; + } + + for (int i = 0; i < says.size(); i++) { + if (!says.get(i).getMarkup().equals(utt.says.get(i).getMarkup())) { + return false; + } + } + return true; + } + + /** + * Can be set to true or false, true indicating that the user provided only a string to the API, + * at which the system will not issue a warning if the synthesizer falls back onto the plain + * text when the synthesizer does not support Markup. + */ + public Utterance setNoWarningOnFallback(boolean noWarning) { + mNoWarningOnFallback = noWarning; + return this; + } +} diff --git a/core/java/android/text/method/QwertyKeyListener.java b/core/java/android/text/method/QwertyKeyListener.java index 0bd46bc..b17f502 100644 --- a/core/java/android/text/method/QwertyKeyListener.java +++ b/core/java/android/text/method/QwertyKeyListener.java @@ -115,7 +115,7 @@ public class QwertyKeyListener extends BaseKeyListener { if (count > 0 && selStart == selEnd && selStart > 0) { char c = content.charAt(selStart - 1); - if (c == i || c == Character.toUpperCase(i) && view != null) { + if ((c == i || c == Character.toUpperCase(i)) && view != null) { if (showCharacterPicker(view, content, c, false, count)) { resetMetaState(content); return true; diff --git a/core/java/android/transition/ChangeTransform.java b/core/java/android/transition/ChangeTransform.java index 85cb2c7..8c03e82 100644 --- a/core/java/android/transition/ChangeTransform.java +++ b/core/java/android/transition/ChangeTransform.java @@ -69,7 +69,7 @@ public class ChangeTransform extends Transition { public void set(View view, float[] values) { for (int i = 0; i < values.length; i++) { float value = values[i]; - if (value != Float.NaN) { + if (!Float.isNaN(value)) { sChangedProperties[i].setValue(view, value); } } diff --git a/core/java/android/transition/MoveImage.java b/core/java/android/transition/MoveImage.java index 183cdd2..6f1b6f7 100644 --- a/core/java/android/transition/MoveImage.java +++ b/core/java/android/transition/MoveImage.java @@ -64,7 +64,13 @@ public class MoveImage extends Transition { if (!(view instanceof ImageView) || view.getVisibility() != View.VISIBLE) { return; } + ImageView imageView = (ImageView) view; + Drawable drawable = imageView.getDrawable(); + if (drawable == null) { + return; + } Map<String, Object> values = transitionValues.values; + values.put(PROPNAME_DRAWABLE, drawable); ViewGroup parent = (ViewGroup) view.getParent(); parent.getLocationInWindow(mTempLoc); @@ -79,11 +85,9 @@ public class MoveImage extends Transition { 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 diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 49a0138..9a70099 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -89,7 +89,8 @@ import java.util.List; * 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>, <code>targetClass</code>, - * <code>excludeId</code>, or <code>excludeClass</code>, which this transition acts upon. + * <code>targetViewName</code>, <code>excludeId</code>, <code>excludeClass</code>, or + * <code>excludeViewName</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 @@ -106,6 +107,40 @@ public abstract class Transition implements Cloneable { private static final String LOG_TAG = "Transition"; static final boolean DBG = false; + /** + * With {@link #setMatchOrder(int...)}, chooses to match by View instance. + */ + public static final int MATCH_INSTANCE = 0x1; + private static final int MATCH_FIRST = MATCH_INSTANCE; + + /** + * With {@link #setMatchOrder(int...)}, chooses to match by + * {@link android.view.View#getViewName()}. Null names will not be matched. + */ + public static final int MATCH_VIEW_NAME = 0x2; + + /** + * With {@link #setMatchOrder(int...)}, chooses to match by + * {@link android.view.View#getId()}. Negative IDs will not be matched. + */ + public static final int MATCH_ID = 0x3; + + /** + * With {@link #setMatchOrder(int...)}, chooses to match by the {@link android.widget.Adapter} + * item id. When {@link android.widget.Adapter#hasStableIds()} returns false, no match + * will be made for items. + */ + public static final int MATCH_ITEM_ID = 0x4; + + private static final int MATCH_LAST = MATCH_ITEM_ID; + + private static final int[] DEFAULT_MATCH_ORDER = { + MATCH_VIEW_NAME, + MATCH_INSTANCE, + MATCH_ID, + MATCH_ITEM_ID, + }; + private String mName = getClass().getName(); long mStartDelay = -1; @@ -113,16 +148,19 @@ public abstract class Transition implements Cloneable { TimeInterpolator mInterpolator = null; ArrayList<Integer> mTargetIds = new ArrayList<Integer>(); ArrayList<View> mTargets = new ArrayList<View>(); + ArrayList<String> mTargetNames = null; + ArrayList<Class> mTargetTypes = null; ArrayList<Integer> mTargetIdExcludes = null; ArrayList<View> mTargetExcludes = null; ArrayList<Class> mTargetTypeExcludes = null; - ArrayList<Class> mTargetTypes = null; + ArrayList<String> mTargetNameExcludes = null; ArrayList<Integer> mTargetIdChildExcludes = null; ArrayList<View> mTargetChildExcludes = null; ArrayList<Class> mTargetTypeChildExcludes = null; private TransitionValuesMaps mStartValues = new TransitionValuesMaps(); private TransitionValuesMaps mEndValues = new TransitionValuesMaps(); TransitionSet mParent = null; + private int[] mMatchOrder = DEFAULT_MATCH_ORDER; // Per-animator information used for later canceling when future transitions overlap private static ThreadLocal<ArrayMap<Animator, AnimationInfo>> sRunningAnimators = @@ -334,6 +372,211 @@ public abstract class Transition implements Cloneable { } /** + * Sets the order in which Transition matches View start and end values. + * <p> + * The default behavior is to match first by {@link android.view.View#getViewName()}, + * then by View instance, then by {@link android.view.View#getId()} and finally + * by its item ID if it is in a direct child of ListView. The caller can + * choose to have only some or all of the values of {@link #MATCH_INSTANCE}, + * {@link #MATCH_VIEW_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. Only + * the match algorithms supplied will be used to determine whether Views are the + * the same in both the start and end Scene. Views that do not match will be considered + * as entering or leaving the Scene. + * </p> + * @param matches A list of zero or more of {@link #MATCH_INSTANCE}, + * {@link #MATCH_VIEW_NAME}, {@link #MATCH_ITEM_ID}, and {@link #MATCH_ID}. + * If none are provided, then the default match order will be set. + */ + public void setMatchOrder(int... matches) { + if (matches == null || matches.length == 0) { + mMatchOrder = DEFAULT_MATCH_ORDER; + } else { + for (int i = 0; i < matches.length; i++) { + int match = matches[i]; + if (!isValidMatch(match)) { + throw new IllegalArgumentException("matches contains invalid value"); + } + if (alreadyContains(matches, i)) { + throw new IllegalArgumentException("matches contains a duplicate value"); + } + } + mMatchOrder = matches.clone(); + } + } + + private static boolean isValidMatch(int match) { + return (match >= MATCH_FIRST && match <= MATCH_LAST); + } + + private static boolean alreadyContains(int[] array, int searchIndex) { + int value = array[searchIndex]; + for (int i = 0; i < searchIndex; i++) { + if (array[i] == value) { + return true; + } + } + return false; + } + + /** + * Match start/end values by View instance. Adds matched values to startValuesList + * and endValuesList and removes them from unmatchedStart and unmatchedEnd. + */ + private void matchInstances(ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList, + ArrayMap<View, TransitionValues> unmatchedStart, + ArrayMap<View, TransitionValues> unmatchedEnd) { + for (int i = unmatchedStart.size() - 1; i >= 0; i--) { + View view = unmatchedStart.keyAt(i); + TransitionValues end = unmatchedEnd.remove(view); + if (end != null) { + TransitionValues start = unmatchedStart.removeAt(i); + startValuesList.add(start); + endValuesList.add(end); + } + } + } + + /** + * Match start/end values by Adapter item ID. Adds matched values to startValuesList + * and endValuesList and removes them from unmatchedStart and unmatchedEnd, using + * startItemIds and endItemIds as a guide for which Views have unique item IDs. + */ + private void matchItemIds(ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList, + ArrayMap<View, TransitionValues> unmatchedStart, + ArrayMap<View, TransitionValues> unmatchedEnd, + LongSparseArray<View> startItemIds, LongSparseArray<View> endItemIds) { + int numStartIds = startItemIds.size(); + for (int i = 0; i < numStartIds; i++) { + View startView = startItemIds.valueAt(i); + if (startView != null) { + View endView = endItemIds.get(startItemIds.keyAt(i)); + if (endView != null) { + TransitionValues startValues = unmatchedStart.get(startView); + TransitionValues endValues = unmatchedEnd.get(endView); + if (startValues != null && endValues != null) { + startValuesList.add(startValues); + endValuesList.add(endValues); + unmatchedStart.remove(startView); + unmatchedEnd.remove(endView); + } + } + } + } + } + + /** + * Match start/end values by Adapter view ID. Adds matched values to startValuesList + * and endValuesList and removes them from unmatchedStart and unmatchedEnd, using + * startIds and endIds as a guide for which Views have unique IDs. + */ + private void matchIds(ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList, + ArrayMap<View, TransitionValues> unmatchedStart, + ArrayMap<View, TransitionValues> unmatchedEnd, + SparseArray<View> startIds, SparseArray<View> endIds) { + int numStartIds = startIds.size(); + for (int i = 0; i < numStartIds; i++) { + View startView = startIds.valueAt(i); + if (startView != null && isValidTarget(startView)) { + View endView = endIds.get(startIds.keyAt(i)); + if (endView != null && isValidTarget(endView)) { + TransitionValues startValues = unmatchedStart.get(startView); + TransitionValues endValues = unmatchedEnd.get(endView); + if (startValues != null && endValues != null) { + startValuesList.add(startValues); + endValuesList.add(endValues); + unmatchedStart.remove(startView); + unmatchedEnd.remove(endView); + } + } + } + } + } + + /** + * Match start/end values by Adapter viewName. Adds matched values to startValuesList + * and endValuesList and removes them from unmatchedStart and unmatchedEnd, using + * startNames and endNames as a guide for which Views have unique viewNames. + */ + private void matchNames(ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList, + ArrayMap<View, TransitionValues> unmatchedStart, + ArrayMap<View, TransitionValues> unmatchedEnd, + ArrayMap<String, View> startNames, ArrayMap<String, View> endNames) { + int numStartNames = startNames.size(); + for (int i = 0; i < numStartNames; i++) { + View startView = startNames.valueAt(i); + if (startView != null && isValidTarget(startView)) { + View endView = endNames.get(startNames.keyAt(i)); + if (endView != null && isValidTarget(endView)) { + TransitionValues startValues = unmatchedStart.get(startView); + TransitionValues endValues = unmatchedEnd.get(endView); + if (startValues != null && endValues != null) { + startValuesList.add(startValues); + endValuesList.add(endValues); + unmatchedStart.remove(startView); + unmatchedEnd.remove(endView); + } + } + } + } + } + + /** + * Adds all values from unmatchedStart and unmatchedEnd to startValuesList and endValuesList, + * assuming that there is no match between values in the list. + */ + private void addUnmatched(ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList, + ArrayMap<View, TransitionValues> unmatchedStart, + ArrayMap<View, TransitionValues> unmatchedEnd) { + // Views that only exist in the start Scene + for (int i = 0; i < unmatchedStart.size(); i++) { + startValuesList.add(unmatchedStart.valueAt(i)); + endValuesList.add(null); + } + + // Views that only exist in the end Scene + for (int i = 0; i < unmatchedEnd.size(); i++) { + endValuesList.add(unmatchedEnd.valueAt(i)); + startValuesList.add(null); + } + } + + private void matchStartAndEnd(TransitionValuesMaps startValues, + TransitionValuesMaps endValues, + ArrayList<TransitionValues> startValuesList, + ArrayList<TransitionValues> endValuesList) { + ArrayMap<View, TransitionValues> unmatchedStart = + new ArrayMap<View, TransitionValues>(startValues.viewValues); + ArrayMap<View, TransitionValues> unmatchedEnd = + new ArrayMap<View, TransitionValues>(endValues.viewValues); + + for (int i = 0; i < mMatchOrder.length; i++) { + switch (mMatchOrder[i]) { + case MATCH_INSTANCE: + matchInstances(startValuesList, endValuesList, unmatchedStart, unmatchedEnd); + break; + case MATCH_VIEW_NAME: + matchNames(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, + startValues.nameValues, endValues.nameValues); + break; + case MATCH_ID: + matchIds(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, + startValues.idValues, endValues.idValues); + break; + case MATCH_ITEM_ID: + matchItemIds(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, + startValues.itemIdValues, endValues.itemIdValues); + break; + } + } + addUnmatched(startValuesList, endValuesList, unmatchedStart, unmatchedEnd); + } + + /** * This method, essentially a wrapper around all calls to createAnimator for all * possible target views, is called with the entire set of start/end * values. The implementation in Transition iterates through these lists @@ -349,76 +592,10 @@ public abstract class Transition implements Cloneable { if (DBG) { Log.d(LOG_TAG, "createAnimators() for " + this); } - ArrayMap<View, TransitionValues> endCopy = - new ArrayMap<View, TransitionValues>(endValues.viewValues); - SparseArray<TransitionValues> endIdCopy = endValues.idValues.clone(); - LongSparseArray<TransitionValues> endItemIdCopy = endValues.itemIdValues.clone(); - // Walk through the start values, playing everything we find - // Remove from the end set as we go ArrayList<TransitionValues> startValuesList = new ArrayList<TransitionValues>(); ArrayList<TransitionValues> endValuesList = new ArrayList<TransitionValues>(); - for (View view : startValues.viewValues.keySet()) { - TransitionValues start = null; - TransitionValues end = null; - boolean isInListView = false; - if (view.getParent() instanceof ListView) { - isInListView = true; - } - if (!isInListView) { - int id = view.getId(); - start = startValues.viewValues.get(view); - end = endValues.viewValues.get(view); - if (end != null) { - endCopy.remove(view); - } else if (id != View.NO_ID) { - end = endIdCopy.get(id); - if (end == null || startValues.viewValues.containsKey(end.view)) { - end = null; - id = View.NO_ID; - } else { - endCopy.remove(end.view); - } - } - endIdCopy.remove(id); - if (isValidTarget(view, id)) { - startValuesList.add(start); - endValuesList.add(end); - } - } else { - ListView parent = (ListView) view.getParent(); - if (parent.getAdapter().hasStableIds()) { - int position = parent.getPositionForView(view); - long itemId = parent.getItemIdAtPosition(position); - start = startValues.itemIdValues.get(itemId); - endItemIdCopy.remove(itemId); - // TODO: deal with targetIDs for itemIDs for ListView items - startValuesList.add(start); - endValuesList.add(end); - } - } - } - int startItemIdCopySize = startValues.itemIdValues.size(); - for (int i = 0; i < startItemIdCopySize; ++i) { - long id = startValues.itemIdValues.keyAt(i); - if (isValidTarget(null, id)) { - TransitionValues start = startValues.itemIdValues.get(id); - TransitionValues end = endValues.itemIdValues.get(id); - endItemIdCopy.remove(id); - startValuesList.add(start); - endValuesList.add(end); - } - } - // Now walk through the remains of the end set - // We've already matched everything from start to end, everything else doesn't match. - for (View view : endCopy.keySet()) { - int id = view.getId(); - if (isValidTarget(view, id)) { - TransitionValues start = null; - TransitionValues end = endCopy.get(view); - startValuesList.add(start); - endValuesList.add(end); - } - } + matchStartAndEnd(startValues, endValues, startValuesList, endValuesList); + ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); long minStartDelay = Long.MAX_VALUE; int minAnimator = mAnimators.size(); @@ -520,7 +697,8 @@ public abstract class Transition implements Cloneable { * is not checked (this is in the case of ListView items, where the * views are ignored and only the ids are used). */ - boolean isValidTarget(View target, long targetId) { + boolean isValidTarget(View target) { + int targetId = target.getId(); if (mTargetIdExcludes != null && mTargetIdExcludes.contains(targetId)) { return false; } @@ -536,10 +714,20 @@ public abstract class Transition implements Cloneable { } } } - if (mTargetIds.size() == 0 && mTargets.size() == 0 && mTargetTypes == null) { + if (mTargetNameExcludes != null && target != null && target.getViewName() != null) { + if (mTargetNameExcludes.contains(target.getViewName())) { + return false; + } + } + if (mTargetIds.size() == 0 && mTargets.size() == 0 && + (mTargetTypes == null || mTargetTypes.isEmpty() && + (mTargetNames == null || mTargetNames.isEmpty()))) { + return true; + } + if (mTargetIds.contains(targetId) || mTargets.contains(target)) { return true; } - if (mTargetIds.contains((int) targetId) || mTargets.contains(target)) { + if (mTargetNames != null && mTargetNames.contains(target.getViewName())) { return true; } if (mTargetTypes != null) { @@ -690,6 +878,33 @@ public abstract class Transition implements Cloneable { } /** + * Adds the viewName of a target view that this Transition is interested in + * animating. By default, there are no targetNames, and a Transition will + * listen for changes on every view in the hierarchy below the sceneRoot + * of the Scene being transitioned into. Setting targetNames constrains + * the Transition to only listen for, and act on, views with these viewNames. + * Views with different viewNames, or no viewName whatsoever, will be ignored. + * + * <p>Note that viewNames should be unique within the view hierarchy.</p> + * + * @see android.view.View#getViewName() + * @param targetName The viewName of a target view, must be non-null. + * @return The Transition to which the target viewName is added. + * Returning the same object makes it easier to chain calls during + * construction, such as + * <code>transitionSet.addTransitions(new Fade()).addTarget(someName);</code> + */ + public Transition addTarget(String targetName) { + if (targetName != null) { + if (mTargetNames != null) { + mTargetNames = new ArrayList<String>(); + } + mTargetNames.add(targetName); + } + return this; + } + + /** * 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 @@ -712,10 +927,12 @@ public abstract class Transition implements Cloneable { * <code>transitionSet.addTransitions(new Fade()).addTarget(ImageView.class);</code> */ public Transition addTarget(Class targetType) { - if (mTargetTypes == null) { - mTargetTypes = new ArrayList<Class>(); + if (targetType != null) { + if (mTargetTypes == null) { + mTargetTypes = new ArrayList<Class>(); + } + mTargetTypes.add(targetType); } - mTargetTypes.add(targetType); return this; } @@ -737,6 +954,23 @@ public abstract class Transition implements Cloneable { } /** + * Removes the given targetName from the list of viewNames that this Transition + * is interested in animating. + * + * @param targetName The viewName of a target view, must not be null. + * @return The Transition from which the targetName is removed. + * Returning the same object makes it easier to chain calls during + * construction, such as + * <code>transitionSet.addTransitions(new Fade()).removeTargetName(someName);</code> + */ + public Transition removeTarget(String targetName) { + if (targetName != null && mTargetNames != null) { + mTargetNames.remove(targetName); + } + return this; + } + + /** * Whether to add the given id to the list of target ids to exclude from this * transition. The <code>exclude</code> parameter specifies whether the target * should be added to or removed from the excluded list. @@ -758,7 +992,35 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeTarget(int targetId, boolean exclude) { - mTargetIdExcludes = excludeId(mTargetIdExcludes, targetId, exclude); + if (targetId >= 0) { + mTargetIdExcludes = excludeObject(mTargetIdExcludes, targetId, exclude); + } + return this; + } + + /** + * Whether to add the given viewName to the list of target viewNames to exclude from this + * transition. The <code>exclude</code> parameter specifies whether the target + * should be added to or removed from the excluded list. + * + * <p>Excluding targets is a general mechanism for allowing transitions to run on + * a view hierarchy while skipping target views that should not be part of + * the transition. For example, you may want to avoid animating children + * of a specific ListView or Spinner. Views can be excluded by their + * id, their instance reference, their viewName, or by the Class of that view + * (eg, {@link Spinner}).</p> + * + * @see #excludeTarget(View, boolean) + * @see #excludeTarget(int, boolean) + * @see #excludeTarget(Class, boolean) + * + * @param targetViewName The name of a target to ignore when running this transition. + * @param exclude Whether to add the target to or remove the target from the + * current list of excluded targets. + * @return This transition object. + */ + public Transition excludeTarget(String targetViewName, boolean exclude) { + mTargetNameExcludes = excludeObject(mTargetNameExcludes, targetViewName, exclude); return this; } @@ -788,23 +1050,10 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeChildren(int targetId, boolean exclude) { - mTargetIdChildExcludes = excludeId(mTargetIdChildExcludes, targetId, exclude); - return this; - } - - /** - * Utility method to manage the boilerplate code that is the same whether we - * are excluding targets or their children. - */ - private ArrayList<Integer> excludeId(ArrayList<Integer> list, int targetId, boolean exclude) { - if (targetId > 0) { - if (exclude) { - list = ArrayListManager.add(list, targetId); - } else { - list = ArrayListManager.remove(list, targetId); - } + if (targetId >= 0) { + mTargetIdChildExcludes = excludeObject(mTargetIdChildExcludes, targetId, exclude); } - return list; + return this; } /** @@ -829,7 +1078,7 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeTarget(View target, boolean exclude) { - mTargetExcludes = excludeView(mTargetExcludes, target, exclude); + mTargetExcludes = excludeObject(mTargetExcludes, target, exclude); return this; } @@ -855,7 +1104,7 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeChildren(View target, boolean exclude) { - mTargetChildExcludes = excludeView(mTargetChildExcludes, target, exclude); + mTargetChildExcludes = excludeObject(mTargetChildExcludes, target, exclude); return this; } @@ -863,7 +1112,7 @@ public abstract class Transition implements Cloneable { * Utility method to manage the boilerplate code that is the same whether we * are excluding targets or their children. */ - private ArrayList<View> excludeView(ArrayList<View> list, View target, boolean exclude) { + private static <T> ArrayList<T> excludeObject(ArrayList<T> list, T target, boolean exclude) { if (target != null) { if (exclude) { list = ArrayListManager.add(list, target); @@ -896,7 +1145,7 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeTarget(Class type, boolean exclude) { - mTargetTypeExcludes = excludeType(mTargetTypeExcludes, type, exclude); + mTargetTypeExcludes = excludeObject(mTargetTypeExcludes, type, exclude); return this; } @@ -923,26 +1172,11 @@ public abstract class Transition implements Cloneable { * @return This transition object. */ public Transition excludeChildren(Class type, boolean exclude) { - mTargetTypeChildExcludes = excludeType(mTargetTypeChildExcludes, type, exclude); + mTargetTypeChildExcludes = excludeObject(mTargetTypeChildExcludes, type, exclude); return this; } /** - * Utility method to manage the boilerplate code that is the same whether we - * are excluding targets or their children. - */ - private ArrayList<Class> excludeType(ArrayList<Class> list, Class type, boolean exclude) { - if (type != null) { - if (exclude) { - list = ArrayListManager.add(list, type); - } else { - list = ArrayListManager.remove(list, type); - } - } - return list; - } - - /** * Sets the target view instances that this Transition is interested in * animating. By default, there are no targets, and a Transition will * listen for changes on every view in the hierarchy below the sceneRoot @@ -991,9 +1225,27 @@ public abstract class Transition implements Cloneable { } /** - * Returns the array of target IDs that this transition limits itself to - * tracking and animating. If the array is null for both this method and - * {@link #getTargets()}, then this transition is + * Removes the given target from the list of targets that this Transition + * is interested in animating. + * + * @param target The type of the target view, must be non-null. + * @return Transition The Transition from which the target is removed. + * Returning the same object makes it easier to chain calls during + * construction, such as + * <code>transitionSet.addTransitions(new Fade()).removeTarget(someType);</code> + */ + public Transition removeTarget(Class target) { + if (target != null) { + mTargetTypes.remove(target); + } + return this; + } + + /** + * Returns the list of target IDs that this transition limits itself to + * tracking and animating. If the list is null or empty for + * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetViewNames()}, and + * {@link #getTargetTypes()} then this transition is * not limited to specific views, and will handle changes to any views * in the hierarchy of a scene change. * @@ -1004,9 +1256,10 @@ public abstract class Transition implements Cloneable { } /** - * Returns the array of target views that this transition limits itself to - * tracking and animating. If the array is null for both this method and - * {@link #getTargetIds()}, then this transition is + * Returns the list of target views that this transition limits itself to + * tracking and animating. If the list is null or empty for + * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetViewNames()}, and + * {@link #getTargetTypes()} then this transition is * not limited to specific views, and will handle changes to any views * in the hierarchy of a scene change. * @@ -1017,6 +1270,34 @@ public abstract class Transition implements Cloneable { } /** + * Returns the list of target viewNames that this transition limits itself to + * tracking and animating. If the list is null or empty for + * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetViewNames()}, and + * {@link #getTargetTypes()} then this transition is + * not limited to specific views, and will handle changes to any views + * in the hierarchy of a scene change. + * + * @return the list of target viewNames + */ + public List<String> getTargetViewNames() { + return mTargetNames; + } + + /** + * Returns the list of target viewNames that this transition limits itself to + * tracking and animating. If the list is null or empty for + * {@link #getTargetIds()}, {@link #getTargets()}, {@link #getTargetViewNames()}, and + * {@link #getTargetTypes()} then this transition is + * not limited to specific views, and will handle changes to any views + * in the hierarchy of a scene change. + * + * @return the list of target Types + */ + public List<Class> getTargetTypes() { + return mTargetTypes; + } + + /** * Recursive method that captures values for the given view and the * hierarchy underneath it. * @param sceneRoot The root of the view hierarchy being captured @@ -1025,52 +1306,42 @@ public abstract class Transition implements Cloneable { */ void captureValues(ViewGroup sceneRoot, boolean start) { clearValues(start); - if (mTargetIds.size() > 0 || mTargets.size() > 0) { - if (mTargetIds.size() > 0) { - for (int i = 0; i < mTargetIds.size(); ++i) { - int id = mTargetIds.get(i); - View view = sceneRoot.findViewById(id); - if (view != null) { - TransitionValues values = new TransitionValues(); - values.view = view; - if (start) { - captureStartValues(values); - } else { - captureEndValues(values); - } - capturePropagationValues(values); - if (start) { - mStartValues.viewValues.put(view, values); - if (id >= 0) { - mStartValues.idValues.put(id, values); - } - } else { - mEndValues.viewValues.put(view, values); - if (id >= 0) { - mEndValues.idValues.put(id, values); - } - } + if ((mTargetIds.size() > 0 || mTargets.size() > 0) + && (mTargetNames == null || mTargetNames.isEmpty()) + && (mTargetTypes == null || mTargetTypes.isEmpty())) { + for (int i = 0; i < mTargetIds.size(); ++i) { + int id = mTargetIds.get(i); + View view = sceneRoot.findViewById(id); + if (view != null) { + TransitionValues values = new TransitionValues(); + values.view = view; + if (start) { + captureStartValues(values); + } else { + captureEndValues(values); + } + capturePropagationValues(values); + if (start) { + addViewValues(mStartValues, view, values); + } else { + addViewValues(mEndValues, view, values); } } } - if (mTargets.size() > 0) { - for (int i = 0; i < mTargets.size(); ++i) { - View view = mTargets.get(i); - if (view != null) { - TransitionValues values = new TransitionValues(); - values.view = view; - if (start) { - captureStartValues(values); - } else { - captureEndValues(values); - } - capturePropagationValues(values); - if (start) { - mStartValues.viewValues.put(view, values); - } else { - mEndValues.viewValues.put(view, values); - } - } + for (int i = 0; i < mTargets.size(); ++i) { + View view = mTargets.get(i); + TransitionValues values = new TransitionValues(); + values.view = view; + if (start) { + captureStartValues(values); + } else { + captureEndValues(values); + } + capturePropagationValues(values); + if (start) { + mStartValues.viewValues.put(view, values); + } else { + mEndValues.viewValues.put(view, values); } } } else { @@ -1078,6 +1349,47 @@ public abstract class Transition implements Cloneable { } } + static void addViewValues(TransitionValuesMaps transitionValuesMaps, + View view, TransitionValues transitionValues) { + transitionValuesMaps.viewValues.put(view, transitionValues); + int id = view.getId(); + if (id >= 0) { + if (transitionValuesMaps.idValues.indexOfKey(id) >= 0) { + // Duplicate IDs cannot match by ID. + transitionValuesMaps.idValues.put(id, null); + } else { + transitionValuesMaps.idValues.put(id, view); + } + } + String name = view.getViewName(); + if (name != null) { + if (transitionValuesMaps.nameValues.containsKey(name)) { + // Duplicate viewNames: cannot match by viewName. + transitionValuesMaps.nameValues.put(name, null); + } else { + transitionValuesMaps.nameValues.put(name, view); + } + } + if (view.getParent() instanceof ListView) { + ListView listview = (ListView) view.getParent(); + if (listview.getAdapter().hasStableIds()) { + int position = listview.getPositionForView(view); + long itemId = listview.getItemIdAtPosition(position); + if (transitionValuesMaps.itemIdValues.indexOfKey(itemId) >= 0) { + // Duplicate item IDs: cannot match by item ID. + View alreadyMatched = transitionValuesMaps.itemIdValues.get(itemId); + if (alreadyMatched != null) { + alreadyMatched.setHasTransientState(false); + transitionValuesMaps.itemIdValues.put(itemId, null); + } + } else { + view.setHasTransientState(true); + transitionValuesMaps.itemIdValues.put(itemId, view); + } + } + } + } + /** * Clear valuesMaps for specified start/end state * @@ -1109,24 +1421,7 @@ public abstract class Transition implements Cloneable { if (view == null) { return; } - boolean isListViewItem = false; - if (view.getParent() instanceof ListView) { - isListViewItem = true; - } - if (isListViewItem && !((ListView) view.getParent()).getAdapter().hasStableIds()) { - // ignore listview children unless we can track them with stable IDs - return; - } - int id = View.NO_ID; - long itemId = View.NO_ID; - if (!isListViewItem) { - id = view.getId(); - } else { - ListView listview = (ListView) view.getParent(); - int position = listview.getPositionForView(view); - itemId = listview.getItemIdAtPosition(position); - view.setHasTransientState(true); - } + int id = view.getId(); if (mTargetIdExcludes != null && mTargetIdExcludes.contains(id)) { return; } @@ -1151,23 +1446,9 @@ public abstract class Transition implements Cloneable { } 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); - } + addViewValues(mStartValues, view, values); } else { - if (!isListViewItem) { - mEndValues.viewValues.put(view, values); - if (id >= 0) { - mEndValues.idValues.put((int) id, values); - } - } else { - mEndValues.itemIdValues.put(itemId, values); - } + addViewValues(mEndValues, view, values); } } if (view instanceof ViewGroup) { @@ -1178,7 +1459,7 @@ public abstract class Transition implements Cloneable { if (mTargetChildExcludes != null && mTargetChildExcludes.contains(view)) { return; } - if (mTargetTypeChildExcludes != null && view != null) { + if (mTargetTypeChildExcludes != null) { int numTypes = mTargetTypeChildExcludes.size(); for (int i = 0; i < numTypes; ++i) { if (mTargetTypeChildExcludes.get(i).isInstance(view)) { @@ -1204,22 +1485,7 @@ public abstract class Transition implements Cloneable { return mParent.getTransitionValues(view, start); } TransitionValuesMaps valuesMaps = start ? mStartValues : mEndValues; - TransitionValues values = valuesMaps.viewValues.get(view); - if (values == null) { - int id = view.getId(); - if (id >= 0) { - values = valuesMaps.idValues.get(id); - } - if (values == null && view.getParent() instanceof ListView) { - ListView listview = (ListView) view.getParent(); - int position = listview.getPositionForView(view); - long itemId = listview.getItemIdAtPosition(position); - values = valuesMaps.itemIdValues.get(itemId); - } - // TODO: Doesn't handle the case where a view was parented to a - // ListView (with an itemId), but no longer is - } - return values; + return valuesMaps.viewValues.get(view); } /** @@ -1303,11 +1569,7 @@ public abstract class Transition implements Cloneable { boolean cancel = false; TransitionValues oldValues = oldInfo.values; View oldView = oldInfo.view; - TransitionValues newValues = mEndValues.viewValues != null ? - mEndValues.viewValues.get(oldView) : null; - if (newValues == null) { - newValues = mEndValues.idValues.get(oldView.getId()); - } + TransitionValues newValues = mEndValues.viewValues.get(oldView); if (oldValues != null) { // if oldValues null, then transition didn't care to stash values, // and won't get canceled @@ -1429,17 +1691,15 @@ public abstract class Transition implements Cloneable { } } for (int i = 0; i < mStartValues.itemIdValues.size(); ++i) { - TransitionValues tv = mStartValues.itemIdValues.valueAt(i); - View v = tv.view; - if (v.hasTransientState()) { - v.setHasTransientState(false); + View view = mStartValues.itemIdValues.valueAt(i); + if (view != null) { + view.setHasTransientState(false); } } for (int i = 0; i < mEndValues.itemIdValues.size(); ++i) { - TransitionValues tv = mEndValues.itemIdValues.valueAt(i); - View v = tv.view; - if (v.hasTransientState()) { - v.setHasTransientState(false); + View view = mEndValues.itemIdValues.valueAt(i); + if (view != null) { + view.setHasTransientState(false); } } mEnded = true; diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index a5e960a..f4b562f 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -30,6 +30,7 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; +import java.util.StringTokenizer; /** * This class inflates scenes and transitions from resource files. @@ -40,6 +41,10 @@ import java.util.ArrayList; * and {@link android.R.styleable#TransitionManager}. */ public class TransitionInflater { + private static final String MATCH_INSTANCE = "instance"; + private static final String MATCH_VIEW_NAME = "viewName"; + private static final String MATCH_ID = "id"; + private static final String MATCH_ITEM_ID = "itemId"; private Context mContext; @@ -229,11 +234,20 @@ public class TransitionInflater { com.android.internal.R.styleable.TransitionTarget); int id = a.getResourceId( com.android.internal.R.styleable.TransitionTarget_targetId, -1); + String viewName; if (id >= 0) { transition.addTarget(id); } else if ((id = a.getResourceId( com.android.internal.R.styleable.TransitionTarget_excludeId, -1)) >= 0) { transition.excludeTarget(id, true); + } else if ((viewName = a.getString( + com.android.internal.R.styleable.TransitionTarget_targetViewName)) + != null) { + transition.addTarget(viewName); + } else if ((viewName = a.getString( + com.android.internal.R.styleable.TransitionTarget_excludeViewName)) + != null) { + transition.excludeTarget(viewName, true); } else { String className = a.getString( com.android.internal.R.styleable.TransitionTarget_excludeClass); @@ -257,6 +271,33 @@ public class TransitionInflater { } } + private int[] parseMatchOrder(String matchOrderString) { + StringTokenizer st = new StringTokenizer(matchOrderString, ","); + int matches[] = new int[st.countTokens()]; + int index = 0; + while (st.hasMoreTokens()) { + String token = st.nextToken().trim(); + if (MATCH_ID.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_ID; + } else if (MATCH_INSTANCE.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_INSTANCE; + } else if (MATCH_VIEW_NAME.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_VIEW_NAME; + } else if (MATCH_ITEM_ID.equalsIgnoreCase(token)) { + matches[index] = Transition.MATCH_ITEM_ID; + } else if (token.isEmpty()) { + int[] smallerMatches = new int[matches.length - 1]; + System.arraycopy(matches, 0, smallerMatches, 0, index); + matches = smallerMatches; + index--; + } else { + throw new RuntimeException("Unknown match type in matchOrder: '" + token + "'"); + } + index++; + } + return matches; + } + private Transition loadTransition(Transition transition, AttributeSet attrs) throws Resources.NotFoundException { @@ -275,6 +316,11 @@ public class TransitionInflater { if (resID > 0) { transition.setInterpolator(AnimationUtils.loadInterpolator(mContext, resID)); } + String matchOrder = + a.getString(com.android.internal.R.styleable.Transition_matchOrder); + if (matchOrder != null) { + transition.setMatchOrder(parseMatchOrder(matchOrder)); + } a.recycle(); return transition; } diff --git a/core/java/android/transition/TransitionSet.java b/core/java/android/transition/TransitionSet.java index 9081234..698b563 100644 --- a/core/java/android/transition/TransitionSet.java +++ b/core/java/android/transition/TransitionSet.java @@ -272,24 +272,8 @@ public class TransitionSet extends Transition { 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); + if (isValidTarget(view)) { + addViewValues(included, view, values.viewValues.valueAt(i)); } } return included; @@ -328,10 +312,9 @@ public class TransitionSet extends Transition { @Override public void captureStartValues(TransitionValues transitionValues) { - int targetId = transitionValues.view.getId(); - if (isValidTarget(transitionValues.view, targetId)) { + if (isValidTarget(transitionValues.view)) { for (Transition childTransition : mTransitions) { - if (childTransition.isValidTarget(transitionValues.view, targetId)) { + if (childTransition.isValidTarget(transitionValues.view)) { childTransition.captureStartValues(transitionValues); } } @@ -340,10 +323,9 @@ public class TransitionSet extends Transition { @Override public void captureEndValues(TransitionValues transitionValues) { - int targetId = transitionValues.view.getId(); - if (isValidTarget(transitionValues.view, targetId)) { + if (isValidTarget(transitionValues.view)) { for (Transition childTransition : mTransitions) { - if (childTransition.isValidTarget(transitionValues.view, targetId)) { + if (childTransition.isValidTarget(transitionValues.view)) { childTransition.captureEndValues(transitionValues); } } diff --git a/core/java/android/transition/TransitionValuesMaps.java b/core/java/android/transition/TransitionValuesMaps.java index 131596b..6d5700a 100644 --- a/core/java/android/transition/TransitionValuesMaps.java +++ b/core/java/android/transition/TransitionValuesMaps.java @@ -24,7 +24,7 @@ import android.view.View; class TransitionValuesMaps { ArrayMap<View, TransitionValues> viewValues = new ArrayMap<View, TransitionValues>(); - SparseArray<TransitionValues> idValues = new SparseArray<TransitionValues>(); - LongSparseArray<TransitionValues> itemIdValues = - new LongSparseArray<TransitionValues>(); + SparseArray<View> idValues = new SparseArray<View>(); + LongSparseArray<View> itemIdValues = new LongSparseArray<View>(); + ArrayMap<String, View> nameValues = new ArrayMap<String, View>(); } diff --git a/core/java/android/transition/Visibility.java b/core/java/android/transition/Visibility.java index 6e6496c..0f7638b 100644 --- a/core/java/android/transition/Visibility.java +++ b/core/java/android/transition/Visibility.java @@ -162,26 +162,15 @@ public abstract class Visibility extends Transition { public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues, TransitionValues endValues) { VisibilityInfo visInfo = getVisibilityChangeInfo(startValues, endValues); - if (visInfo.visibilityChange) { - // Only transition views that are either targets of this transition - // or whose parent hierarchies remain stable between scenes - boolean isTarget = false; - if (mTargets.size() > 0 || mTargetIds.size() > 0) { - View startView = startValues != null ? startValues.view : null; - View endView = endValues != null ? endValues.view : null; - int startId = startView != null ? startView.getId() : -1; - int endId = endView != null ? endView.getId() : -1; - isTarget = isValidTarget(startView, startId) || isValidTarget(endView, endId); - } - if (isTarget || ((visInfo.startParent != null || visInfo.endParent != null))) { - if (visInfo.fadeIn) { - return onAppear(sceneRoot, startValues, visInfo.startVisibility, - endValues, visInfo.endVisibility); - } else { - return onDisappear(sceneRoot, startValues, visInfo.startVisibility, - endValues, visInfo.endVisibility - ); - } + if (visInfo.visibilityChange + && (visInfo.startParent != null || visInfo.endParent != null)) { + if (visInfo.fadeIn) { + return onAppear(sceneRoot, startValues, visInfo.startVisibility, + endValues, visInfo.endVisibility); + } else { + return onDisappear(sceneRoot, startValues, visInfo.startVisibility, + endValues, visInfo.endVisibility + ); } } return null; diff --git a/core/java/android/tv/ITvInputClient.aidl b/core/java/android/tv/ITvInputClient.aidl deleted file mode 100644 index 538f8a1..0000000 --- a/core/java/android/tv/ITvInputClient.aidl +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 deleted file mode 100644 index a4c99e4..0000000 --- a/core/java/android/tv/ITvInputManager.aidl +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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/ITvInputSession.aidl b/core/java/android/tv/ITvInputSession.aidl deleted file mode 100644 index 32fee4b..0000000 --- a/core/java/android/tv/ITvInputSession.aidl +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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/ITvInputSessionWrapper.java b/core/java/android/tv/ITvInputSessionWrapper.java deleted file mode 100644 index 3ccccf3..0000000 --- a/core/java/android/tv/ITvInputSessionWrapper.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.java b/core/java/android/tv/TvInputInfo.java deleted file mode 100644 index 90e4177..0000000 --- a/core/java/android/tv/TvInputInfo.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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 deleted file mode 100644 index 7b9b1fb..0000000 --- a/core/java/android/tv/TvInputManager.java +++ /dev/null @@ -1,742 +0,0 @@ -/* - * 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 deleted file mode 100644 index 70e7f95..0000000 --- a/core/java/android/tv/TvInputService.java +++ /dev/null @@ -1,551 +0,0 @@ -/* - * 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 deleted file mode 100644 index 289823b..0000000 --- a/core/java/android/tv/TvView.java +++ /dev/null @@ -1,360 +0,0 @@ -/* - * 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/Range.java b/core/java/android/util/Range.java index 9a4bd4b..d7e8cf0 100644 --- a/core/java/android/util/Range.java +++ b/core/java/android/util/Range.java @@ -18,7 +18,7 @@ package android.util; import static com.android.internal.util.Preconditions.*; -import android.hardware.camera2.impl.HashCodeHelpers; +import android.hardware.camera2.utils.HashCodeHelpers; /** * Immutable class for describing the range of two numeric values. diff --git a/core/java/android/hardware/camera2/Rational.java b/core/java/android/util/Rational.java index 77b8c26..8d4c67f 100644 --- a/core/java/android/hardware/camera2/Rational.java +++ b/core/java/android/util/Rational.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package android.hardware.camera2; +package android.util; /** - * The rational data type used by CameraMetadata keys. Contains a pair of ints representing the - * numerator and denominator of a Rational number. This type is immutable. + * <p>An immutable data type representation a rational number.</p> + * + * <p>Contains a pair of {@code int}s representing the numerator and denominator of a + * Rational number. </p> */ public final class Rational { private final int mNumerator; @@ -30,7 +32,9 @@ public final class Rational { * is always positive.</p> * * <p>A rational value with a 0-denominator may be constructed, but will have similar semantics - * as float NaN and INF values. The int getter functions return 0 in this case.</p> + * as float {@code NaN} and {@code INF} values. For {@code NaN}, + * both {@link #getNumerator} and {@link #getDenominator} functions will return 0. For + * positive or negative {@code INF}, only the {@link #getDenominator} will return 0.</p> * * @param numerator the numerator of the rational * @param denominator the denominator of the rational @@ -91,14 +95,14 @@ public final class Rational { * <p>A reduced form of a Rational is calculated by dividing both the numerator and the * denominator by their greatest common divisor.</p> * - * <pre> + * <pre>{@code * (new Rational(1, 2)).equals(new Rational(1, 2)) == true // trivially true * (new Rational(2, 3)).equals(new Rational(1, 2)) == false // trivially false * (new Rational(1, 2)).equals(new Rational(2, 4)) == true // true after reduction * (new Rational(0, 0)).equals(new Rational(0, 0)) == true // NaN.equals(NaN) * (new Rational(1, 0)).equals(new Rational(5, 0)) == true // both are +infinity * (new Rational(1, 0)).equals(new Rational(-1, 0)) == false // +infinity != -infinity - * </pre> + * }</pre> * * @param obj a reference to another object * @@ -159,16 +163,15 @@ public final class Rational { return (float) mNumerator / (float) mDenominator; } + /** + * {@inheritDoc} + */ @Override public int hashCode() { - final long INT_MASK = 0xffffffffL; - - long asLong = INT_MASK & mNumerator; - asLong <<= 32; - - asLong |= (INT_MASK & mDenominator); + // Bias the hash code for the first (2^16) values for both numerator and denominator + int numeratorFlipped = mNumerator << 16 | mNumerator >>> 16; - return ((Long)asLong).hashCode(); + return mDenominator ^ numeratorFlipped; } /** diff --git a/core/java/android/util/TimeUtils.java b/core/java/android/util/TimeUtils.java index 33964a0..8f4b710 100644 --- a/core/java/android/util/TimeUtils.java +++ b/core/java/android/util/TimeUtils.java @@ -245,6 +245,9 @@ public class TimeUtils { private static final int SECONDS_PER_HOUR = 60 * 60; private static final int SECONDS_PER_DAY = 24 * 60 * 60; + /** @hide */ + public static final long NANOS_PER_MS = 1000000; + private static final Object sFormatSync = new Object(); private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+5]; diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 0a76075..1066430 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -112,8 +112,6 @@ public final class Choreographer { private static final int SKIPPED_FRAME_WARNING_LIMIT = SystemProperties.getInt( "debug.choreographer.skipwarning", 30); - private static final long NANOS_PER_MS = 1000000; - private static final int MSG_DO_FRAME = 0; private static final int MSG_DO_SCHEDULE_VSYNC = 1; private static final int MSG_DO_SCHEDULE_CALLBACK = 2; @@ -263,7 +261,7 @@ public final class Choreographer { * @return The refresh rate as the nanoseconds between frames * @hide */ - long getFrameIntervalNanos() { + public long getFrameIntervalNanos() { return mFrameIntervalNanos; } @@ -456,7 +454,7 @@ public final class Choreographer { * @hide */ public long getFrameTime() { - return getFrameTimeNanos() / NANOS_PER_MS; + return getFrameTimeNanos() / TimeUtils.NANOS_PER_MS; } /** @@ -497,7 +495,7 @@ public final class Choreographer { } } else { final long nextFrameTime = Math.max( - mLastFrameTimeNanos / NANOS_PER_MS + sFrameDelay, now); + mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG) { Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms."); } @@ -746,7 +744,7 @@ public final class Choreographer { mFrame = frame; Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); - mHandler.sendMessageAtTime(msg, timestampNanos / NANOS_PER_MS); + mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); } @Override diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java index 6c451eb..5056097 100644 --- a/core/java/android/view/GLES20Canvas.java +++ b/core/java/android/view/GLES20Canvas.java @@ -41,10 +41,6 @@ import android.text.TextUtils; * An implementation of Canvas on top of OpenGL ES 2.0. */ class GLES20Canvas extends HardwareCanvas { - // Must match modifiers used in the JNI layer - private static final int MODIFIER_NONE = 0; - private static final int MODIFIER_SHADER = 2; - private final boolean mOpaque; protected long mRenderer; @@ -79,22 +75,10 @@ class GLES20Canvas extends HardwareCanvas { // Constructors /////////////////////////////////////////////////////////////////////////// - /** - * Creates a canvas to render directly on screen. - */ - GLES20Canvas(boolean translucent) { - this(false, translucent); - } - - protected GLES20Canvas(boolean record, boolean translucent) { - mOpaque = !translucent; - - if (record) { - mRenderer = nCreateDisplayListRenderer(); - } else { - mRenderer = nCreateRenderer(); - } - + // TODO: Merge with GLES20RecordingCanvas + protected GLES20Canvas() { + mOpaque = false; + mRenderer = nCreateDisplayListRenderer(); setupFinalizer(); } @@ -106,7 +90,6 @@ class GLES20Canvas extends HardwareCanvas { } } - private static native long nCreateRenderer(); private static native long nCreateDisplayListRenderer(); private static native void nResetDisplayListRenderer(long renderer); private static native void nDestroyRenderer(long renderer); @@ -135,36 +118,6 @@ class GLES20Canvas extends HardwareCanvas { private static native void nSetProperty(String name, String value); /////////////////////////////////////////////////////////////////////////// - // Hardware layers - /////////////////////////////////////////////////////////////////////////// - - @Override - void pushLayerUpdate(HardwareLayer layer) { - nPushLayerUpdate(mRenderer, layer.getLayer()); - } - - @Override - void cancelLayerUpdate(HardwareLayer layer) { - nCancelLayerUpdate(mRenderer, layer.getLayer()); - } - - @Override - void flushLayerUpdates() { - nFlushLayerUpdates(mRenderer); - } - - @Override - void clearLayerUpdates() { - nClearLayerUpdates(mRenderer); - } - - 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); - private static native void nCancelLayerUpdate(long renderer, long layer); - - /////////////////////////////////////////////////////////////////////////// // Canvas management /////////////////////////////////////////////////////////////////////////// @@ -238,20 +191,6 @@ class GLES20Canvas extends HardwareCanvas { private static native void nFinish(long renderer); - /** - * Returns the size of the stencil buffer required by the underlying - * implementation. - * - * @return The minimum number of bits the stencil buffer must. Always >= 0. - * - * @hide - */ - public static int getStencilSize() { - return nGetStencilSize(); - } - - private static native int nGetStencilSize(); - /////////////////////////////////////////////////////////////////////////// // Functor /////////////////////////////////////////////////////////////////////////// @@ -288,49 +227,6 @@ class GLES20Canvas extends HardwareCanvas { */ static final int FLUSH_CACHES_FULL = 2; - /** - * Flush caches to reclaim as much memory as possible. The amount of memory - * to reclaim is indicate by the level parameter. - * - * The level can be one of {@link #FLUSH_CACHES_MODERATE} or - * {@link #FLUSH_CACHES_FULL}. - * - * @param level Hint about the amount of memory to reclaim - */ - static void flushCaches(int level) { - nFlushCaches(level); - } - - private static native void nFlushCaches(int level); - - /** - * Release all resources associated with the underlying caches. This should - * only be called after a full flushCaches(). - * - * @hide - */ - static void terminateCaches() { - nTerminateCaches(); - } - - private static native void nTerminateCaches(); - - static boolean initCaches() { - return nInitCaches(); - } - - private static native boolean nInitCaches(); - - /////////////////////////////////////////////////////////////////////////// - // Atlas - /////////////////////////////////////////////////////////////////////////// - - static void initAtlas(GraphicBuffer buffer, long[] map) { - nInitAtlas(buffer, map, map.length); - } - - private static native void nInitAtlas(GraphicBuffer buffer, long[] map, int count); - /////////////////////////////////////////////////////////////////////////// // Display list /////////////////////////////////////////////////////////////////////////// @@ -650,13 +546,8 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_SHADER); - try { - nDrawArc(mRenderer, oval.left, oval.top, oval.right, oval.bottom, - startAngle, sweepAngle, useCenter, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawArc(mRenderer, oval.left, oval.top, oval.right, oval.bottom, + startAngle, sweepAngle, useCenter, paint.mNativePaint); } private static native void nDrawArc(long renderer, float left, float top, @@ -672,7 +563,6 @@ class GLES20Canvas extends HardwareCanvas { public void drawPatch(NinePatch patch, Rect dst, Paint paint) { Bitmap bitmap = patch.getBitmap(); throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing patches 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); @@ -682,7 +572,6 @@ class GLES20Canvas extends HardwareCanvas { public void drawPatch(NinePatch patch, RectF dst, Paint paint) { Bitmap bitmap = patch.getBitmap(); throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing patches 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); @@ -694,14 +583,8 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, float left, float top, Paint paint) { throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing bitmaps - int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, nativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, nativePaint); } private static native void nDrawBitmap(long renderer, long bitmap, byte[] buffer, @@ -710,15 +593,9 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, Matrix matrix, Paint paint) { throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing bitmaps - int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, - matrix.native_instance, nativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, + matrix.native_instance, nativePaint); } private static native void nDrawBitmap(long renderer, long bitmap, byte[] buffer, @@ -727,55 +604,43 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(Bitmap bitmap, Rect src, Rect dst, Paint paint) { throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing bitmaps - int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - - int left, top, right, bottom; - if (src == null) { - left = top = 0; - right = bitmap.getWidth(); - bottom = bitmap.getHeight(); - } else { - left = src.left; - right = src.right; - top = src.top; - bottom = src.bottom; - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, right, bottom, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + int left, top, right, bottom; + if (src == null) { + left = top = 0; + right = bitmap.getWidth(); + bottom = bitmap.getHeight(); + } else { + left = src.left; + right = src.right; + top = src.top; + bottom = src.bottom; } + + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, right, bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); } @Override public void drawBitmap(Bitmap bitmap, Rect src, RectF dst, Paint paint) { throwIfCannotDraw(bitmap); - // Shaders are ignored when drawing bitmaps - int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - - float left, top, right, bottom; - if (src == null) { - left = top = 0; - right = bitmap.getWidth(); - bottom = bitmap.getHeight(); - } else { - left = src.left; - right = src.right; - top = src.top; - bottom = src.bottom; - } - - nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, right, bottom, - dst.left, dst.top, dst.right, dst.bottom, nativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + + float left, top, right, bottom; + if (src == null) { + left = top = 0; + right = bitmap.getWidth(); + bottom = bitmap.getHeight(); + } else { + left = src.left; + right = src.right; + top = src.top; + bottom = src.bottom; } + + nDrawBitmap(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, left, top, right, bottom, + dst.left, dst.top, dst.right, dst.bottom, nativePaint); } private static native void nDrawBitmap(long renderer, long bitmap, byte[] buffer, @@ -805,7 +670,6 @@ class GLES20Canvas extends HardwareCanvas { throw new ArrayIndexOutOfBoundsException(); } - // Shaders are ignored when drawing bitmaps final long nativePaint = paint == null ? 0 : paint.mNativePaint; nDrawBitmap(mRenderer, colors, offset, stride, x, y, width, height, hasAlpha, nativePaint); @@ -817,7 +681,6 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawBitmap(int[] colors, int offset, int stride, int x, int y, int width, int height, boolean hasAlpha, Paint paint) { - // Shaders are ignored when drawing bitmaps drawBitmap(colors, offset, stride, (float) x, (float) y, width, height, hasAlpha, paint); } @@ -840,14 +703,9 @@ class GLES20Canvas extends HardwareCanvas { checkRange(colors.length, colorOffset, count); } - int modifiers = paint != null ? setupModifiers(bitmap, paint) : MODIFIER_NONE; - try { - final long nativePaint = paint == null ? 0 : paint.mNativePaint; - nDrawBitmapMesh(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, meshWidth, meshHeight, - verts, vertOffset, colors, colorOffset, nativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + final long nativePaint = paint == null ? 0 : paint.mNativePaint; + nDrawBitmapMesh(mRenderer, bitmap.mNativeBitmap, bitmap.mBuffer, meshWidth, meshHeight, + verts, vertOffset, colors, colorOffset, nativePaint); } private static native void nDrawBitmapMesh(long renderer, long bitmap, byte[] buffer, @@ -856,12 +714,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawCircle(float cx, float cy, float radius, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_SHADER); - try { - nDrawCircle(mRenderer, cx, cy, radius, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawCircle(mRenderer, cx, cy, radius, paint.mNativePaint); } private static native void nDrawCircle(long renderer, float cx, float cy, @@ -906,12 +759,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_SHADER); - try { - nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawLines(mRenderer, pts, offset, count, paint.mNativePaint); } private static native void nDrawLines(long renderer, float[] points, @@ -924,12 +772,7 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawOval(RectF oval, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_SHADER); - try { - nDrawOval(mRenderer, oval.left, oval.top, oval.right, oval.bottom, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawOval(mRenderer, oval.left, oval.top, oval.right, oval.bottom, paint.mNativePaint); } private static native void nDrawOval(long renderer, float left, float top, @@ -944,34 +787,18 @@ class GLES20Canvas extends HardwareCanvas { @Override public void drawPath(Path path, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_SHADER); - try { - if (path.isSimplePath) { - if (path.rects != null) { - nDrawRects(mRenderer, path.rects.mNativeRegion, paint.mNativePaint); - } - } else { - nDrawPath(mRenderer, path.mNativePath, paint.mNativePaint); + if (path.isSimplePath) { + if (path.rects != null) { + nDrawRects(mRenderer, path.rects.mNativeRegion, paint.mNativePaint); } - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + } else { + nDrawPath(mRenderer, path.mNativePath, paint.mNativePaint); } } private static native void nDrawPath(long renderer, long path, long paint); private static native void nDrawRects(long renderer, long region, long paint); - void drawRects(float[] rects, int count, Paint paint) { - int modifiers = setupModifiers(paint, MODIFIER_SHADER); - try { - nDrawRects(mRenderer, rects, count, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } - } - - private static native void nDrawRects(long renderer, float[] rects, int count, long paint); - @Override public void drawPicture(Picture picture) { if (picture.createdFromStream) { @@ -1029,12 +856,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_SHADER); - try { - nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawPoints(mRenderer, pts, offset, count, paint.mNativePaint); } private static native void nDrawPoints(long renderer, float[] points, @@ -1047,12 +869,7 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - nDrawPosText(mRenderer, text, index, count, pos, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawPosText(mRenderer, text, index, count, pos, paint.mNativePaint); } private static native void nDrawPosText(long renderer, char[] text, int index, int count, @@ -1065,12 +882,7 @@ class GLES20Canvas extends HardwareCanvas { throw new ArrayIndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - nDrawPosText(mRenderer, text, 0, text.length(), pos, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawPosText(mRenderer, text, 0, text.length(), pos, paint.mNativePaint); } private static native void nDrawPosText(long renderer, String text, int start, int end, @@ -1079,12 +891,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_SHADER); - try { - nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawRect(mRenderer, left, top, right, bottom, paint.mNativePaint); } private static native void nDrawRect(long renderer, float left, float top, @@ -1108,12 +915,7 @@ class GLES20Canvas extends HardwareCanvas { @Override 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, left, top, right, bottom, rx, ry, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawRoundRect(mRenderer, left, top, right, bottom, rx, ry, paint.mNativePaint); } private static native void nDrawRoundRect(long renderer, float left, float top, @@ -1125,37 +927,27 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - nDrawText(mRenderer, text, index, count, x, y, paint.mBidiFlags, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawText(mRenderer, text, index, count, x, y, + paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); } private static native void nDrawText(long renderer, char[] text, int index, int count, - float x, float y, int bidiFlags, long paint); + float x, float y, int bidiFlags, long paint, long typeface); @Override public void drawText(CharSequence text, int start, int end, float x, float y, Paint paint) { - int modifiers = setupModifiers(paint); - try { - if (text instanceof String || text instanceof SpannedString || - text instanceof SpannableString) { - nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mBidiFlags, - paint.mNativePaint); - } else if (text instanceof GraphicsOperations) { - ((GraphicsOperations) text).drawText(this, start, end, x, y, - paint); - } else { - char[] buf = TemporaryBuffer.obtain(end - start); - TextUtils.getChars(text, start, end, buf, 0); - nDrawText(mRenderer, buf, 0, end - start, x, y, - paint.mBidiFlags, paint.mNativePaint); - TemporaryBuffer.recycle(buf); - } - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + nDrawText(mRenderer, text.toString(), start, end, x, y, paint.mBidiFlags, + paint.mNativePaint, paint.mNativeTypeface); + } else if (text instanceof GraphicsOperations) { + ((GraphicsOperations) text).drawText(this, start, end, x, y, paint); + } else { + char[] buf = TemporaryBuffer.obtain(end - start); + TextUtils.getChars(text, start, end, buf, 0); + nDrawText(mRenderer, buf, 0, end - start, x, y, + paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); + TemporaryBuffer.recycle(buf); } } @@ -1165,26 +957,17 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - nDrawText(mRenderer, text, start, end, x, y, paint.mBidiFlags, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawText(mRenderer, text, start, end, x, y, + paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); } private static native void nDrawText(long renderer, String text, int start, int end, - float x, float y, int bidiFlags, long paint); + float x, float y, int bidiFlags, long paint, long typeface); @Override public void drawText(String text, float x, float y, Paint paint) { - int modifiers = setupModifiers(paint); - try { - nDrawText(mRenderer, text, 0, text.length(), x, y, paint.mBidiFlags, - paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawText(mRenderer, text, 0, text.length(), x, y, + paint.mBidiFlags, paint.mNativePaint, paint.mNativeTypeface); } @Override @@ -1194,13 +977,8 @@ class GLES20Canvas extends HardwareCanvas { throw new ArrayIndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - nDrawTextOnPath(mRenderer, text, index, count, path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawTextOnPath(mRenderer, text, index, count, path.mNativePath, hOffset, vOffset, + paint.mBidiFlags, paint.mNativePaint); } private static native void nDrawTextOnPath(long renderer, char[] text, int index, int count, @@ -1210,13 +988,8 @@ class GLES20Canvas extends HardwareCanvas { public void drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint) { if (text.length() == 0) return; - int modifiers = setupModifiers(paint); - try { - nDrawTextOnPath(mRenderer, text, 0, text.length(), path.mNativePath, hOffset, vOffset, - paint.mBidiFlags, paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawTextOnPath(mRenderer, text, 0, text.length(), path.mNativePath, hOffset, vOffset, + paint.mBidiFlags, paint.mNativePaint); } private static native void nDrawTextOnPath(long renderer, String text, int start, int end, @@ -1232,17 +1005,12 @@ class GLES20Canvas extends HardwareCanvas { throw new IllegalArgumentException("Unknown direction: " + dir); } - int modifiers = setupModifiers(paint); - try { - nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, dir, - paint.mNativePaint); - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); - } + nDrawTextRun(mRenderer, text, index, count, contextIndex, contextCount, x, y, dir, + paint.mNativePaint, paint.mNativeTypeface); } private static native void nDrawTextRun(long renderer, char[] text, int index, int count, - int contextIndex, int contextCount, float x, float y, int dir, long nativePaint); + int contextIndex, int contextCount, float x, float y, int dir, long nativePaint, long nativeTypeface); @Override public void drawTextRun(CharSequence text, int start, int end, int contextStart, int contextEnd, @@ -1251,32 +1019,27 @@ class GLES20Canvas extends HardwareCanvas { throw new IndexOutOfBoundsException(); } - int modifiers = setupModifiers(paint); - try { - int flags = dir == 0 ? 0 : 1; - if (text instanceof String || text instanceof SpannedString || - text instanceof SpannableString) { - nDrawTextRun(mRenderer, text.toString(), start, end, contextStart, - contextEnd, x, y, flags, paint.mNativePaint); - } else if (text instanceof GraphicsOperations) { - ((GraphicsOperations) text).drawTextRun(this, start, end, - contextStart, contextEnd, x, y, flags, paint); - } else { - int contextLen = contextEnd - contextStart; - int len = end - start; - char[] buf = TemporaryBuffer.obtain(contextLen); - TextUtils.getChars(text, contextStart, contextEnd, buf, 0); - nDrawTextRun(mRenderer, buf, start - contextStart, len, 0, contextLen, - x, y, flags, paint.mNativePaint); - TemporaryBuffer.recycle(buf); - } - } finally { - if (modifiers != MODIFIER_NONE) nResetModifiers(mRenderer, modifiers); + int flags = dir == 0 ? 0 : 1; + if (text instanceof String || text instanceof SpannedString || + text instanceof SpannableString) { + nDrawTextRun(mRenderer, text.toString(), start, end, contextStart, + contextEnd, x, y, flags, paint.mNativePaint, paint.mNativeTypeface); + } else if (text instanceof GraphicsOperations) { + ((GraphicsOperations) text).drawTextRun(this, start, end, + contextStart, contextEnd, x, y, flags, paint); + } else { + int contextLen = contextEnd - contextStart; + int len = end - start; + char[] buf = TemporaryBuffer.obtain(contextLen); + TextUtils.getChars(text, contextStart, contextEnd, buf, 0); + nDrawTextRun(mRenderer, buf, start - contextStart, len, 0, contextLen, + x, y, flags, paint.mNativePaint, paint.mNativeTypeface); + TemporaryBuffer.recycle(buf); } } private static native void nDrawTextRun(long renderer, String text, int start, int end, - int contextStart, int contextEnd, float x, float y, int flags, long nativePaint); + int contextStart, int contextEnd, float x, float y, int flags, long nativePaint, long nativeTypeface); @Override public void drawVertices(VertexMode mode, int vertexCount, float[] verts, int vertOffset, @@ -1284,40 +1047,4 @@ class GLES20Canvas extends HardwareCanvas { int indexOffset, int indexCount, Paint paint) { // TODO: Implement } - - private int setupModifiers(Bitmap b, Paint paint) { - if (b.getConfig() != Bitmap.Config.ALPHA_8) { - return MODIFIER_NONE; - } else { - return setupModifiers(paint); - } - } - - private int setupModifiers(Paint paint) { - int modifiers = MODIFIER_NONE; - - final Shader shader = paint.getShader(); - if (shader != null) { - nSetupShader(mRenderer, shader.native_shader); - modifiers |= MODIFIER_SHADER; - } - - return modifiers; - } - - private int setupModifiers(Paint paint, int flags) { - int modifiers = MODIFIER_NONE; - - final Shader shader = paint.getShader(); - if (shader != null && (flags & MODIFIER_SHADER) != 0) { - nSetupShader(mRenderer, shader.native_shader); - modifiers |= MODIFIER_SHADER; - } - - return modifiers; - } - - private static native void nSetupShader(long renderer, long shader); - - private static native void nResetModifiers(long renderer, int modifiers); } diff --git a/core/java/android/view/GLES20RecordingCanvas.java b/core/java/android/view/GLES20RecordingCanvas.java index a94ec3a..b2961e5 100644 --- a/core/java/android/view/GLES20RecordingCanvas.java +++ b/core/java/android/view/GLES20RecordingCanvas.java @@ -36,7 +36,7 @@ class GLES20RecordingCanvas extends GLES20Canvas { RenderNode mNode; private GLES20RecordingCanvas() { - super(true, true); + super(); } static GLES20RecordingCanvas obtain(@NonNull RenderNode node) { diff --git a/core/java/android/view/GLRenderer.java b/core/java/android/view/GLRenderer.java deleted file mode 100644 index 7b49006..0000000 --- a/core/java/android/view/GLRenderer.java +++ /dev/null @@ -1,1537 +0,0 @@ -/* - * 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 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 - void setOpaque(boolean opaque) { - // Not supported - } - - @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; - } - - @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); - } - } - } - - /** - * 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/HardwareCanvas.java b/core/java/android/view/HardwareCanvas.java index 9568760..b8e7d8c 100644 --- a/core/java/android/view/HardwareCanvas.java +++ b/core/java/android/view/HardwareCanvas.java @@ -110,48 +110,6 @@ public abstract class HardwareCanvas extends Canvas { return RenderNode.STATUS_DONE; } - /** - * Indicates that the specified layer must be updated as soon as possible. - * - * @param layer The layer to update - * - * @see #clearLayerUpdates() - * - * @hide - */ - 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) - * @see #clearLayerUpdates() - * - * @hide - */ - abstract void cancelLayerUpdate(HardwareLayer layer); - - /** - * Immediately executes all enqueued layer updates. - * - * @see #pushLayerUpdate(HardwareLayer) - * - * @hide - */ - abstract void flushLayerUpdates(); - - /** - * Removes all enqueued layer updates. - * - * @see #pushLayerUpdate(HardwareLayer) - * - * @hide - */ - abstract void clearLayerUpdates(); - public abstract void drawCircle(CanvasProperty<Float> cx, CanvasProperty<Float> cy, CanvasProperty<Float> radius, CanvasProperty<Paint> paint); } diff --git a/core/java/android/view/HardwareLayer.java b/core/java/android/view/HardwareLayer.java index 4d78733..6acb134 100644 --- a/core/java/android/view/HardwareLayer.java +++ b/core/java/android/view/HardwareLayer.java @@ -22,6 +22,8 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import com.android.internal.util.VirtualRefBasePtr; + /** * A hardware layer can be used to render graphics operations into a hardware * friendly buffer. For instance, with an OpenGL backend a hardware layer @@ -36,7 +38,7 @@ final class HardwareLayer { private static final int LAYER_TYPE_DISPLAY_LIST = 2; private HardwareRenderer mRenderer; - private Finalizer mFinalizer; + private VirtualRefBasePtr mFinalizer; private RenderNode mDisplayList; private final int mLayerType; @@ -47,10 +49,7 @@ final class HardwareLayer { } mRenderer = renderer; mLayerType = type; - mFinalizer = new Finalizer(deferredUpdater); - - // Layer is considered initialized at this point, notify the HardwareRenderer - mRenderer.onLayerCreated(this); + mFinalizer = new VirtualRefBasePtr(deferredUpdater); } private void assertType(int type) { @@ -59,6 +58,10 @@ final class HardwareLayer { } } + boolean hasDisplayList() { + return mDisplayList != null; + } + /** * Update the paint used when drawing this layer. * @@ -66,7 +69,8 @@ final class HardwareLayer { * @see View#setLayerPaint(android.graphics.Paint) */ public void setLayerPaint(Paint paint) { - nSetLayerPaint(mFinalizer.mDeferredUpdater, paint.mNativePaint); + nSetLayerPaint(mFinalizer.get(), paint.mNativePaint); + mRenderer.pushLayerUpdate(this); } /** @@ -75,7 +79,7 @@ final class HardwareLayer { * @return True if the layer can be rendered into, false otherwise */ public boolean isValid() { - return mFinalizer != null && mFinalizer.mDeferredUpdater != 0; + return mFinalizer != null && mFinalizer.get() != 0; } /** @@ -91,35 +95,14 @@ final class HardwareLayer { mDisplayList.destroyDisplayListData(); mDisplayList = null; } - if (mRenderer != null) { - mRenderer.onLayerDestroyed(this); - mRenderer = null; - } - doDestroyLayerUpdater(); + mRenderer.onLayerDestroyed(this); + mRenderer = null; + mFinalizer.release(); + mFinalizer = null; } public long getDeferredLayerUpdater() { - return mFinalizer.mDeferredUpdater; - } - - /** - * 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. - * - * It is safe to call this in onLayerDestroyed only - */ - public long detachBackingLayer() { - long backingLayer = nDetachBackingLayer(mFinalizer.mDeferredUpdater); - doDestroyLayerUpdater(); - return backingLayer; - } - - private void doDestroyLayerUpdater() { - if (mFinalizer != null) { - mFinalizer.destroy(); - mFinalizer = null; - } + return mFinalizer.get(); } public RenderNode startRecording() { @@ -132,7 +115,7 @@ final class HardwareLayer { } public void endRecording(Rect dirtyRect) { - nUpdateRenderLayer(mFinalizer.mDeferredUpdater, mDisplayList.getNativeDisplayList(), + nUpdateRenderLayer(mFinalizer.get(), mDisplayList.getNativeDisplayList(), dirtyRect.left, dirtyRect.top, dirtyRect.right, dirtyRect.bottom); mRenderer.pushLayerUpdate(this); } @@ -160,7 +143,7 @@ final class HardwareLayer { * match the desired values. */ public boolean prepare(int width, int height, boolean isOpaque) { - return nPrepare(mFinalizer.mDeferredUpdater, width, height, isOpaque); + return nPrepare(mFinalizer.get(), width, height, isOpaque); } /** @@ -169,7 +152,8 @@ final class HardwareLayer { * @param matrix The transform to apply to the layer. */ public void setTransform(Matrix matrix) { - nSetTransform(mFinalizer.mDeferredUpdater, matrix.native_instance); + nSetTransform(mFinalizer.get(), matrix.native_instance); + mRenderer.pushLayerUpdate(this); } /** @@ -183,41 +167,25 @@ final class HardwareLayer { surface.detachFromGLContext(); // SurfaceTexture owns the texture name and detachFromGLContext // should have deleted it - nOnTextureDestroyed(mFinalizer.mDeferredUpdater); + nOnTextureDestroyed(mFinalizer.get()); } }); } - /** - * 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 - */ - @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); + return nGetLayer(mFinalizer.get()); } public void setSurfaceTexture(SurfaceTexture surface) { assertType(LAYER_TYPE_TEXTURE); - nSetSurfaceTexture(mFinalizer.mDeferredUpdater, surface, false); + nSetSurfaceTexture(mFinalizer.get(), surface, false); + mRenderer.pushLayerUpdate(this); } public void updateSurfaceTexture() { assertType(LAYER_TYPE_TEXTURE); - nUpdateSurfaceTexture(mFinalizer.mDeferredUpdater); + nUpdateSurfaceTexture(mFinalizer.get()); + mRenderer.pushLayerUpdate(this); } /** @@ -225,48 +193,20 @@ final class HardwareLayer { */ SurfaceTexture createSurfaceTexture() { assertType(LAYER_TYPE_TEXTURE); - SurfaceTexture st = new SurfaceTexture(nGetTexName(mFinalizer.mDeferredUpdater)); - nSetSurfaceTexture(mFinalizer.mDeferredUpdater, st, true); + SurfaceTexture st = new SurfaceTexture(nGetTexName(mFinalizer.get())); + nSetSurfaceTexture(mFinalizer.get(), st, true); return st; } - /** - * This should only be used by HardwareRenderer! Do not call directly - */ - 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); } - /** - * This should only be used by HardwareRenderer! Do not call directly - */ - static HardwareLayer createDisplayListLayer(HardwareRenderer renderer, - int width, int height) { - return new HardwareLayer(renderer, nCreateRenderLayer(width, height), LAYER_TYPE_DISPLAY_LIST); - } - 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. - */ - 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); @@ -281,28 +221,4 @@ final class HardwareLayer { 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 e366697..d67c974 100644 --- a/core/java/android/view/HardwareRenderer.java +++ b/core/java/android/view/HardwareRenderer.java @@ -17,13 +17,13 @@ package android.view; import android.graphics.Bitmap; -import android.graphics.Paint; import android.graphics.Rect; import android.graphics.SurfaceTexture; import android.util.DisplayMetrics; import android.view.Surface.OutOfResourcesException; import java.io.File; +import java.io.FileDescriptor; import java.io.PrintWriter; /** @@ -61,11 +61,9 @@ public abstract class HardwareRenderer { * Possible values: * "true", to enable profiling * "visual_bars", to enable profiling and visualize the results on screen - * "visual_lines", to enable profiling and visualize the results on screen * "false", to disable profiling * * @see #PROFILE_PROPERTY_VISUALIZE_BARS - * @see #PROFILE_PROPERTY_VISUALIZE_LINES * * @hide */ @@ -80,14 +78,6 @@ public abstract class HardwareRenderer { public static final String PROFILE_PROPERTY_VISUALIZE_BARS = "visual_bars"; /** - * Value for {@link #PROFILE_PROPERTY}. When the property is set to this - * value, profiling data will be visualized on screen as a line chart. - * - * @hide - */ - public static final String PROFILE_PROPERTY_VISUALIZE_LINES = "visual_lines"; - - /** * System property used to specify the number of frames to be used * when doing hardware rendering profiling. * The default value of this property is #PROFILE_MAX_FRAMES. @@ -181,9 +171,6 @@ public abstract class HardwareRenderer { */ public static boolean sSystemRendererDisabled = false; - /** @hide */ - public static boolean sUseRenderThread = true; - private boolean mEnabled; private boolean mRequested = true; @@ -273,12 +260,16 @@ public abstract class HardwareRenderer { * * @param width Width of the drawing surface. * @param height Height of the drawing surface. + * @param lightX X position of the shadow casting light + * @param lightY Y position of the shadow casting light + * @param lightZ Z position of the shadow casting light + * @param lightRadius radius of the shadow casting light */ - abstract void setup(int width, int height); + abstract void setup(int width, int height, float lightX, float lightY, float lightZ, float lightRadius); /** * Gets the current width of the surface. This is the width that the surface - * was last set to in a call to {@link #setup(int, int)}. + * was last set to in a call to {@link #setup(int, int, float, float, float, float)}. * * @return the current width of the surface */ @@ -286,7 +277,7 @@ public abstract class HardwareRenderer { /** * Gets the current height of the surface. This is the height that the surface - * was last set to in a call to {@link #setup(int, int)}. + * was last set to in a call to {@link #setup(int, int, float, float, float, float)}. * * @return the current width of the surface */ @@ -294,25 +285,14 @@ public abstract class HardwareRenderer { /** * Outputs extra debugging information in the specified file descriptor. - * @param pw - */ - abstract void dumpGfxInfo(PrintWriter pw); - - /** - * Outputs the total number of frames rendered (used for fps calculations) - * - * @return the number of frames rendered */ - abstract long getFrameCount(); + abstract void dumpGfxInfo(PrintWriter pw, FileDescriptor fd); /** * Loads system properties used by the renderer. This method is invoked * whenever system properties are modified. Implementations can use this * to trigger live updates of the renderer based on properties. * - * @param surface The surface to update with the new properties. - * Can be null. - * * @return True if a property has changed. */ abstract boolean loadSystemProperties(); @@ -326,7 +306,7 @@ public abstract class HardwareRenderer { * @hide */ public static void setupDiskCache(File cacheDir) { - GLRenderer.setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); + ThreadedRenderer.setupShadersDiskCache(new File(cacheDir, CACHE_PATH_SHADERS).getAbsolutePath()); } /** @@ -340,12 +320,6 @@ public abstract class HardwareRenderer { abstract void pushLayerUpdate(HardwareLayer layer); /** - * 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 onLayerCreated(HardwareLayer hardwareLayer); - - /** * Tells the HardwareRenderer that the layer is destroyed. The renderer * should remove the layer from any update queues. */ @@ -443,17 +417,18 @@ public abstract class HardwareRenderer { * @param width The width of the drawing surface. * @param height The height of the drawing surface. * @param surface The surface to hardware accelerate + * @param metrics The display metrics used to draw the output. * * @return true if the surface was initialized, false otherwise. Returning * false might mean that the surface was already initialized. */ - boolean initializeIfNeeded(int width, int height, Surface surface) + boolean initializeIfNeeded(int width, int height, Surface surface, DisplayMetrics metrics) throws OutOfResourcesException { if (isRequested()) { // We lost the gl context, so recreate it. if (!isEnabled()) { if (initialize(surface)) { - setup(width, height); + setup(width, height, metrics); return true; } } @@ -461,6 +436,14 @@ public abstract class HardwareRenderer { return false; } + void setup(int width, int height, DisplayMetrics metrics) { + float lightX = width / 2.0f; + float lightY = -400 * metrics.density; + float lightZ = 800 * metrics.density; + float lightRadius = 800 * metrics.density; + setup(width, height, lightX, lightY, lightZ, lightRadius); + } + /** * Optional, sets the name of the renderer. Useful for debugging purposes. * @@ -483,11 +466,7 @@ public abstract class HardwareRenderer { static HardwareRenderer create(boolean translucent) { HardwareRenderer renderer = null; if (GLES20Canvas.isAvailable()) { - if (sUseRenderThread) { - renderer = new ThreadedRenderer(translucent); - } else { - renderer = new GLRenderer(translucent); - } + renderer = new ThreadedRenderer(translucent); } return renderer; } @@ -514,7 +493,7 @@ public abstract class HardwareRenderer { * see {@link android.content.ComponentCallbacks} */ static void startTrimMemory(int level) { - GLRenderer.startTrimMemory(level); + ThreadedRenderer.startTrimMemory(level); } /** @@ -522,7 +501,7 @@ public abstract class HardwareRenderer { * cleanup special resources used by the memory trimming process. */ static void endTrimMemory() { - GLRenderer.endTrimMemory(); + ThreadedRenderer.endTrimMemory(); } /** @@ -569,96 +548,8 @@ public abstract class HardwareRenderer { 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. + * Called by {@link ViewRootImpl} when a new performTraverals is scheduled. */ - abstract class GraphDataProvider { - /** - * Draws the graph as bars. Frame elements are stacked on top of - * each other. - */ - public static final int GRAPH_TYPE_BARS = 0; - /** - * Draws the graph as lines. The number of series drawn corresponds - * to the number of elements. - */ - public static final int GRAPH_TYPE_LINES = 1; - - /** - * Returns the type of graph to render. - * - * @return {@link #GRAPH_TYPE_BARS} or {@link #GRAPH_TYPE_LINES} - */ - abstract int getGraphType(); - - /** - * This method is invoked before the graph is drawn. This method - * can be used to compute sizes, etc. - * - * @param metrics The display metrics - */ - abstract void prepare(DisplayMetrics metrics); - - /** - * @return The size in pixels of a vertical unit. - */ - abstract int getVerticalUnitSize(); - - /** - * @return The size in pixels of a horizontal unit. - */ - abstract int getHorizontalUnitSize(); - - /** - * @return The size in pixels of the margin between horizontal units. - */ - abstract int getHorizontaUnitMargin(); - - /** - * An optional threshold value. - * - * @return A value >= 0 to draw the threshold, a negative value - * to ignore it. - */ - abstract float getThreshold(); - - /** - * The data to draw in the graph. The number of elements in the - * array must be at least {@link #getFrameCount()} * {@link #getElementCount()}. - * If a value is negative the following values will be ignored. - */ - abstract float[] getData(); - - /** - * Returns the number of frames to render in the graph. - */ - abstract int getFrameCount(); - - /** - * Returns the number of elements in each frame. This directly affects - * the number of series drawn in the graph. - */ - abstract int getElementCount(); - - /** - * Returns the current frame, if any. If the returned value is negative - * the current frame is ignored. - */ - abstract int getCurrentFrame(); - - /** - * Prepares the paint to draw the specified element (or series.) - */ - abstract void setupGraphPaint(Paint paint, int elementIndex); - - /** - * Prepares the paint to draw the threshold. - */ - abstract void setupThresholdPaint(Paint paint); - - /** - * Prepares the paint to draw the current frame indicator. - */ - abstract void setupCurrentFramePaint(Paint paint); + public void notifyFramePending() { } } diff --git a/core/java/android/view/IWindowManager.aidl b/core/java/android/view/IWindowManager.aidl index 7d13399..af16185 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -79,7 +79,7 @@ interface IWindowManager void removeWindowToken(IBinder token); void addAppToken(int addPos, IApplicationToken token, int groupId, int stackId, int requestedOrientation, boolean fullscreen, boolean showWhenLocked, int userId, - int configChanges); + int configChanges, boolean voiceInteraction); void setAppGroupId(IBinder token, int groupId); void setAppOrientation(IApplicationToken token, int requestedOrientation); int getAppOrientation(IApplicationToken token); @@ -120,6 +120,7 @@ interface IWindowManager boolean isKeyguardSecure(); boolean inKeyguardRestrictedInputMode(); void dismissKeyguard(); + void keyguardGoingAway(); void closeSystemDialogs(String reason); diff --git a/core/java/android/view/InputDevice.java b/core/java/android/view/InputDevice.java index ae5f37e..358ae8a 100644 --- a/core/java/android/view/InputDevice.java +++ b/core/java/android/view/InputDevice.java @@ -234,6 +234,14 @@ public final class InputDevice implements Parcelable { public static final int SOURCE_JOYSTICK = 0x01000000 | SOURCE_CLASS_JOYSTICK; /** + * The input source is a device connected through HDMI-based bus. + * + * The key comes in through HDMI-CEC or MHL signal line, and is treated as if it were + * generated by a locally connected DPAD or keyboard. + */ + public static final int SOURCE_HDMI = 0x02000000 | SOURCE_CLASS_BUTTON; + + /** * A special input source constant that is used when filtering input devices * to match devices that provide any type of input source. */ diff --git a/core/java/android/view/InputEventConsistencyVerifier.java b/core/java/android/view/InputEventConsistencyVerifier.java index 5dda934..c5e4c21 100644 --- a/core/java/android/view/InputEventConsistencyVerifier.java +++ b/core/java/android/view/InputEventConsistencyVerifier.java @@ -113,7 +113,7 @@ public final class InputEventConsistencyVerifier { * @param flags Flags to the verifier, or 0 if none. */ public InputEventConsistencyVerifier(Object caller, int flags) { - this(caller, flags, InputEventConsistencyVerifier.class.getSimpleName()); + this(caller, flags, null); } /** diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index 2d1016a..8a996d2 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -639,14 +639,31 @@ public class KeyEvent extends InputEvent implements Parcelable { * Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it * has no effect if the device is already awake. */ public static final int KEYCODE_WAKEUP = 224; - - private static final int LAST_KEYCODE = KEYCODE_WAKEUP; + /** Key code constant: Pairing key. + * Initiates peripheral pairing mode. Useful for pairing remote control + * devices or game controllers, especially if no other input mode is + * available. */ + public static final int KEYCODE_PAIRING = 225; + /** Key code constant: Media Top Menu key. + * Goes to the top of media menu. */ + public static final int KEYCODE_MEDIA_TOP_MENU = 226; + /** Key code constant: '11' key. */ + public static final int KEYCODE_11 = 227; + /** Key code constant: '12' key. */ + public static final int KEYCODE_12 = 228; + /** Key code constant: Last Channel key. + * Goes to the last viewed channel. */ + public static final int KEYCODE_LAST_CHANNEL = 229; + /** Key code constant: TV data service key. + * Displays data services like weather, sports. */ + public static final int KEYCODE_TV_DATA_SERVICE = 230; + + private static final int LAST_KEYCODE = KEYCODE_TV_DATA_SERVICE; // 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/InputEventAttributes.h - // external/webkit/WebKit/android/plugins/ANPKeyCodes.h + // frameworks/native/include/input/InputEventLabels.h // frameworks/base/core/res/res/values/attrs.xml // emulator? // LAST_KEYCODE @@ -2698,10 +2715,10 @@ public class KeyEvent extends InputEvent implements Parcelable { public static int keyCodeFromString(String symbolicName) { if (symbolicName.startsWith(LABEL_PREFIX)) { symbolicName = symbolicName.substring(LABEL_PREFIX.length()); - } - int keyCode = nativeKeyCodeFromString(symbolicName); - if (keyCode > 0) { - return keyCode; + int keyCode = nativeKeyCodeFromString(symbolicName); + if (keyCode > 0) { + return keyCode; + } } try { return Integer.parseInt(symbolicName, 10); diff --git a/core/java/android/view/MotionEvent.java b/core/java/android/view/MotionEvent.java index 0626ab9..7f2defd 100644 --- a/core/java/android/view/MotionEvent.java +++ b/core/java/android/view/MotionEvent.java @@ -3070,10 +3070,10 @@ public final class MotionEvent extends InputEvent implements Parcelable { public static int axisFromString(String symbolicName) { if (symbolicName.startsWith(LABEL_PREFIX)) { symbolicName = symbolicName.substring(LABEL_PREFIX.length()); - } - int axis = nativeAxisFromString(symbolicName); - if (axis >= 0) { - return axis; + int axis = nativeAxisFromString(symbolicName); + if (axis >= 0) { + return axis; + } } try { return Integer.parseInt(symbolicName, 10); diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index 0cfde94..e63829e 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/android/view/RenderNode.java @@ -366,10 +366,8 @@ public class RenderNode { * Deep copies the data into native to simplify reference ownership. */ public void setOutline(Outline outline) { - if (outline == null) { + if (outline == null || outline.isEmpty()) { 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); @@ -387,6 +385,10 @@ public class RenderNode { nSetClipToOutline(mNativeRenderNode, clipToOutline); } + public boolean getClipToOutline() { + return nGetClipToOutline(mNativeRenderNode); + } + /** * Controls the RenderNode's circular reveal clip. */ @@ -848,6 +850,13 @@ public class RenderNode { nOutput(mNativeRenderNode); } + /** + * Gets the size of the DisplayList for debug purposes. + */ + public int getDebugSize() { + return nGetDebugSize(mNativeRenderNode); + } + /////////////////////////////////////////////////////////////////////////// // Animations /////////////////////////////////////////////////////////////////////////// @@ -919,6 +928,7 @@ public class RenderNode { private static native void nSetAnimationMatrix(long renderNode, long animationMatrix); private static native boolean nHasOverlappingRendering(long renderNode); + private static native boolean nGetClipToOutline(long renderNode); private static native float nGetAlpha(long renderNode); private static native float nGetLeft(long renderNode); private static native float nGetTop(long renderNode); @@ -938,6 +948,7 @@ public class RenderNode { private static native float nGetPivotX(long renderNode); private static native float nGetPivotY(long renderNode); private static native void nOutput(long renderNode); + private static native int nGetDebugSize(long renderNode); /////////////////////////////////////////////////////////////////////////// // Animations diff --git a/core/java/android/view/RenderNodeAnimator.java b/core/java/android/view/RenderNodeAnimator.java index a675821..4979059 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -16,18 +16,25 @@ package android.view; +import android.animation.Animator; +import android.animation.TimeInterpolator; import android.graphics.Canvas; import android.graphics.CanvasProperty; import android.graphics.Paint; import android.util.SparseIntArray; +import com.android.internal.util.VirtualRefBasePtr; +import com.android.internal.view.animation.FallbackLUTInterpolator; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; + import java.lang.ref.WeakReference; +import java.util.ArrayList; /** * @hide */ -public final class RenderNodeAnimator { - +public final class RenderNodeAnimator extends Animator { // Keep in sync with enum RenderProperty in Animator.h public static final int TRANSLATION_X = 0; public static final int TRANSLATION_Y = 1; @@ -41,9 +48,16 @@ public final class RenderNodeAnimator { public static final int Y = 9; public static final int Z = 10; public static final int ALPHA = 11; + // The last value in the enum, used for array size initialization + public static final int LAST_VALUE = ALPHA; // Keep in sync with enum PaintFields in Animator.h public static final int PAINT_STROKE_WIDTH = 0; + + /** + * Field for the Paint alpha channel, which should be specified as a value + * between 0 and 255. + */ public static final int PAINT_ALPHA = 1; // ViewPropertyAnimator uses a mask for its values, we need to remap them @@ -65,65 +79,207 @@ public final class RenderNodeAnimator { put(ViewPropertyAnimator.ALPHA, ALPHA); }}; - // Keep in sync DeltaValueType in Animator.h - public static final int DELTA_TYPE_ABSOLUTE = 0; - public static final int DELTA_TYPE_DELTA = 1; + private VirtualRefBasePtr mNativePtr; private RenderNode mTarget; - private long mNativePtr; + private View mViewTarget; + private TimeInterpolator mInterpolator; - public int mapViewPropertyToRenderProperty(int viewProperty) { + private boolean mStarted = false; + private boolean mFinished = false; + + public static 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 RenderNodeAnimator(int property, float finalValue) { + init(nCreateAnimator(new WeakReference<RenderNodeAnimator>(this), + property, finalValue)); } - public RenderNodeAnimator(CanvasProperty<Float> property, int deltaType, float deltaValue) { - mNativePtr = nCreateCanvasPropertyFloatAnimator( + public RenderNodeAnimator(CanvasProperty<Float> property, float finalValue) { + init(nCreateCanvasPropertyFloatAnimator( new WeakReference<RenderNodeAnimator>(this), - property.getNativeContainer(), deltaType, deltaValue); + property.getNativeContainer(), finalValue)); } - public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, - int deltaType, float deltaValue) { - mNativePtr = nCreateCanvasPropertyPaintAnimator( + /** + * Creates a new render node animator for a field on a Paint property. + * + * @param property The paint property to target + * @param paintField Paint field to animate, one of {@link #PAINT_ALPHA} or + * {@link #PAINT_STROKE_WIDTH} + * @param finalValue The target value for the property + */ + public RenderNodeAnimator(CanvasProperty<Paint> property, int paintField, float finalValue) { + init(nCreateCanvasPropertyPaintAnimator( new WeakReference<RenderNodeAnimator>(this), - property.getNativeContainer(), paintField, deltaType, deltaValue); + property.getNativeContainer(), paintField, finalValue)); } - public void start(View target) { - mTarget = target.mRenderNode; - mTarget.addAnimator(this); - // Kick off a frame to start the process - target.invalidateViewProperty(true, false); + private void init(long ptr) { + mNativePtr = new VirtualRefBasePtr(ptr); } - public void start(Canvas canvas) { - if (!(canvas instanceof GLES20RecordingCanvas)) { - throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); + private void checkMutable() { + if (mStarted) { + throw new IllegalStateException("Animator has already started, cannot change it now!"); + } + } + + static boolean isNativeInterpolator(TimeInterpolator interpolator) { + return interpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class); + } + + private void applyInterpolator() { + if (mInterpolator == null) return; + + long ni; + if (isNativeInterpolator(mInterpolator)) { + ni = ((NativeInterpolatorFactory)mInterpolator).createNativeInterpolator(); + } else { + long duration = nGetDuration(mNativePtr.get()); + ni = FallbackLUTInterpolator.createNativeInterpolator(mInterpolator, duration); + } + nSetInterpolator(mNativePtr.get(), ni); + } + + @Override + public void start() { + if (mTarget == null) { + throw new IllegalStateException("Missing target!"); } - GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; - mTarget = recordingCanvas.mNode; + + if (mStarted) { + throw new IllegalStateException("Already started!"); + } + + mStarted = true; + applyInterpolator(); mTarget.addAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationStart(this); + } + + if (mViewTarget != null) { + // Kick off a frame to start the process + mViewTarget.invalidateViewProperty(true, false); + } } + @Override public void cancel() { mTarget.removeAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationCancel(this); + } } - public void setDuration(int duration) { - nSetDuration(mNativePtr, duration); + @Override + public void end() { + throw new UnsupportedOperationException(); } - long getNativeAnimator() { - return mNativePtr; + @Override + public void pause() { + throw new UnsupportedOperationException(); + } + + @Override + public void resume() { + throw new UnsupportedOperationException(); + } + + public void setTarget(View view) { + mViewTarget = view; + mTarget = view.mRenderNode; + } + + public void setTarget(Canvas canvas) { + if (!(canvas instanceof GLES20RecordingCanvas)) { + throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); + } + + final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; + setTarget(recordingCanvas.mNode); + } + + public void setTarget(RenderNode node) { + mViewTarget = null; + mTarget = node; + } + + public RenderNode getTarget() { + return mTarget; + } + + /** + * WARNING: May only be called once!!! + * TODO: Fix above -_- + */ + public void setStartValue(float startValue) { + checkMutable(); + nSetStartValue(mNativePtr.get(), startValue); + } + + @Override + public void setStartDelay(long startDelay) { + checkMutable(); + nSetStartDelay(mNativePtr.get(), startDelay); + } + + @Override + public long getStartDelay() { + return nGetStartDelay(mNativePtr.get()); + } + + @Override + public RenderNodeAnimator setDuration(long duration) { + checkMutable(); + nSetDuration(mNativePtr.get(), duration); + return this; + } + + @Override + public long getDuration() { + return nGetDuration(mNativePtr.get()); + } + + @Override + public boolean isRunning() { + return mStarted && !mFinished; + } + + @Override + public void setInterpolator(TimeInterpolator interpolator) { + checkMutable(); + mInterpolator = interpolator; + } + + @Override + public TimeInterpolator getInterpolator() { + return mInterpolator; } private void onFinished() { + mFinished = true; mTarget.removeAnimator(this); + + final ArrayList<AnimatorListener> listeners = getListeners(); + final int numListeners = listeners == null ? 0 : listeners.size(); + for (int i = 0; i < numListeners; i++) { + listeners.get(i).onAnimationEnd(this); + } + } + + long getNativeAnimator() { + return mNativePtr.get(); } // Called by native @@ -134,22 +290,16 @@ public final class RenderNodeAnimator { } } - @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); + int property, float finalValue); private static native long nCreateCanvasPropertyFloatAnimator(WeakReference<RenderNodeAnimator> weakThis, - long canvasProperty, int deltaValueType, float deltaValue); + long canvasProperty, float finalValue); private static native long nCreateCanvasPropertyPaintAnimator(WeakReference<RenderNodeAnimator> weakThis, - long canvasProperty, int paintField, int deltaValueType, float deltaValue); - private static native void nSetDuration(long nativePtr, int duration); - private static native void nUnref(long nativePtr); + long canvasProperty, int paintField, float finalValue); + private static native void nSetStartValue(long nativePtr, float startValue); + private static native void nSetDuration(long nativePtr, long duration); + private static native long nGetDuration(long nativePtr); + private static native void nSetStartDelay(long nativePtr, long startDelay); + private static native long nGetStartDelay(long nativePtr); + private static native void nSetInterpolator(long animPtr, long interpolatorPtr); } diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index c15ce44..5cd3d62 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -38,11 +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, - boolean useIdentityTransform); + Rect sourceCrop, 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, - boolean useIdentityTransform); + Rect sourceCrop, int width, int height, int minLayer, int maxLayer, + boolean allLayers, boolean useIdentityTransform); private static native void nativeOpenTransaction(); private static native void nativeCloseTransaction(); @@ -597,8 +597,8 @@ public class SurfaceControl { public static void screenshot(IBinder display, Surface consumer, int width, int height, int minLayer, int maxLayer, boolean useIdentityTransform) { - screenshot(display, consumer, width, height, minLayer, maxLayer, false, - useIdentityTransform); + screenshot(display, consumer, new Rect(), width, height, minLayer, maxLayer, + false, useIdentityTransform); } /** @@ -613,7 +613,7 @@ public class SurfaceControl { */ public static void screenshot(IBinder display, Surface consumer, int width, int height) { - screenshot(display, consumer, width, height, 0, 0, true, false); + screenshot(display, consumer, new Rect(), width, height, 0, 0, true, false); } /** @@ -623,7 +623,7 @@ 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, false); + screenshot(display, consumer, new Rect(), 0, 0, 0, 0, true, false); } /** @@ -634,6 +634,8 @@ public class SurfaceControl { * the versions that use a {@link Surface} instead, such as * {@link SurfaceControl#screenshot(IBinder, Surface)}. * + * @param sourceCrop The portion of the screen to capture into the Bitmap; + * caller may pass in 'new Rect()' if no cropping is desired. * @param width The desired width of the returned bitmap; the raw * screen will be scaled down to this size. * @param height The desired height of the returned bitmap; the raw @@ -649,13 +651,13 @@ public class SurfaceControl { * 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, - boolean useIdentityTransform) { + public static Bitmap screenshot(Rect sourceCrop, 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, - useIdentityTransform); + return nativeScreenshot(displayToken, sourceCrop, width, height, + minLayer, maxLayer, false, useIdentityTransform); } /** @@ -674,10 +676,10 @@ 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, false); + return nativeScreenshot(displayToken, new Rect(), width, height, 0, 0, true, false); } - private static void screenshot(IBinder display, Surface consumer, + private static void screenshot(IBinder display, Surface consumer, Rect sourceCrop, int width, int height, int minLayer, int maxLayer, boolean allLayers, boolean useIdentityTransform) { if (display == null) { @@ -686,7 +688,7 @@ public class SurfaceControl { if (consumer == null) { throw new IllegalArgumentException("consumer must not be null"); } - nativeScreenshot(display, consumer, width, height, minLayer, maxLayer, allLayers, - useIdentityTransform); + nativeScreenshot(display, consumer, sourceCrop, width, height, + minLayer, maxLayer, allLayers, useIdentityTransform); } } diff --git a/core/java/android/view/TextureView.java b/core/java/android/view/TextureView.java index 1765c43..2a9f7d5 100644 --- a/core/java/android/view/TextureView.java +++ b/core/java/android/view/TextureView.java @@ -405,7 +405,9 @@ public class TextureView extends View { // To cancel updates, the easiest thing to do is simply to remove the // updates listener if (visibility == VISIBLE) { - mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler); + if (mLayer != null) { + mSurface.setOnFrameAvailableListener(mUpdateListener, mAttachInfo.mHandler); + } updateLayerAndInvalidate(); } else { mSurface.setOnFrameAvailableListener(null); diff --git a/core/java/android/view/ThreadedRenderer.java b/core/java/android/view/ThreadedRenderer.java index 2587ba1..9b3ef7f 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -19,17 +19,22 @@ package android.view; import android.graphics.Bitmap; import android.graphics.Rect; import android.graphics.SurfaceTexture; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; import android.os.Trace; +import android.util.Log; +import android.util.TimeUtils; import android.view.Surface.OutOfResourcesException; import android.view.View.AttachInfo; +import java.io.FileDescriptor; 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. @@ -51,21 +56,26 @@ public class ThreadedRenderer extends HardwareRenderer { private static final Rect NULL_RECT = new Rect(); - private static final long NANOS_PER_MS = 1000000; - // Keep in sync with DrawFrameTask.h SYNC_* flags // Nothing interesting to report private static final int SYNC_OK = 0x0; // Needs a ViewRoot invalidate private static final int SYNC_INVALIDATE_REQUIRED = 0x1; + private static final String[] VISUALIZERS = { + PROFILE_PROPERTY_VISUALIZE_BARS, + }; + private int mWidth, mHeight; private long mNativeProxy; private boolean mInitialized = false; private RenderNode mRootNode; private Choreographer mChoreographer; + private boolean mProfilingEnabled; ThreadedRenderer(boolean translucent) { + AtlasInitializer.sInstance.init(); + long rootNodePtr = nCreateRootRenderNode(); mRootNode = RenderNode.adopt(rootNodePtr); mRootNode.setClipToBounds(false); @@ -74,6 +84,8 @@ public class ThreadedRenderer extends HardwareRenderer { // Setup timing mChoreographer = Choreographer.getInstance(); nSetFrameInterval(mNativeProxy, mChoreographer.getFrameIntervalNanos()); + + loadSystemProperties(); } @Override @@ -112,7 +124,7 @@ public class ThreadedRenderer extends HardwareRenderer { @Override void destroyHardwareResources(View view) { destroyResources(view); - // TODO: GLES20Canvas.flushCaches(GLES20Canvas.FLUSH_CACHES_LAYERS); + nFlushCaches(mNativeProxy, GLES20Canvas.FLUSH_CACHES_LAYERS); } private static void destroyResources(View view) { @@ -140,11 +152,11 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override - void setup(int width, int height) { + void setup(int width, int height, float lightX, float lightY, float lightZ, float lightRadius) { mWidth = width; mHeight = height; mRootNode.setLeftTopRightBottom(0, 0, mWidth, mHeight); - nSetup(mNativeProxy, width, height); + nSetup(mNativeProxy, width, height, lightX, lightY, lightZ, lightRadius); } @Override @@ -163,19 +175,33 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override - void dumpGfxInfo(PrintWriter pw) { - // TODO Auto-generated method stub + void dumpGfxInfo(PrintWriter pw, FileDescriptor fd) { + pw.flush(); + nDumpProfileInfo(mNativeProxy, fd); } - @Override - long getFrameCount() { - // TODO Auto-generated method stub - return 0; + 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; + } + + private static boolean checkIfProfilingRequested() { + String profiling = SystemProperties.get(HardwareRenderer.PROFILE_PROPERTY); + int graphType = search(VISUALIZERS, profiling); + return (graphType >= 0) || Boolean.parseBoolean(profiling); } @Override boolean loadSystemProperties() { - return nLoadSystemProperties(mNativeProxy); + boolean changed = nLoadSystemProperties(mNativeProxy); + boolean wantProfiling = checkIfProfilingRequested(); + if (wantProfiling != mProfilingEnabled) { + mProfilingEnabled = wantProfiling; + changed = true; + } + return changed; } private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) { @@ -188,9 +214,11 @@ public class ThreadedRenderer extends HardwareRenderer { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "getDisplayList"); HardwareCanvas canvas = mRootNode.start(mWidth, mHeight); try { - callbacks.onHardwarePostDraw(canvas); + canvas.save(); + callbacks.onHardwarePreDraw(canvas); canvas.drawDisplayList(view.getDisplayList()); callbacks.onHardwarePostDraw(canvas); + canvas.restore(); } finally { mRootNode.end(canvas); Trace.traceEnd(Trace.TRACE_TAG_VIEW); @@ -203,16 +231,26 @@ public class ThreadedRenderer extends HardwareRenderer { void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks, Rect dirty) { attachInfo.mIgnoreDirtyState = true; long frameTimeNanos = mChoreographer.getFrameTimeNanos(); - attachInfo.mDrawingTime = frameTimeNanos / NANOS_PER_MS; + attachInfo.mDrawingTime = frameTimeNanos / TimeUtils.NANOS_PER_MS; + + long recordDuration = 0; + if (mProfilingEnabled) { + recordDuration = System.nanoTime(); + } updateRootDisplayList(view, callbacks); + if (mProfilingEnabled) { + recordDuration = System.nanoTime() - recordDuration; + } + attachInfo.mIgnoreDirtyState = false; if (dirty == null) { dirty = NULL_RECT; } int syncResult = nSyncAndDrawFrame(mNativeProxy, frameTimeNanos, + recordDuration, view.getResources().getDisplayMetrics().density, dirty.left, dirty.top, dirty.right, dirty.bottom); if ((syncResult & SYNC_INVALIDATE_REQUIRED) != 0) { attachInfo.mViewRootImpl.invalidate(); @@ -256,12 +294,7 @@ public class ThreadedRenderer extends HardwareRenderer { @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? + nPushLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater()); } @Override @@ -271,7 +304,7 @@ public class ThreadedRenderer extends HardwareRenderer { @Override void onLayerDestroyed(HardwareLayer layer) { - nDestroyLayer(mNativeProxy, layer.getDeferredLayerUpdater()); + nCancelLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater()); } @Override @@ -284,6 +317,11 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override + public void notifyFramePending() { + nNotifyFramePending(mNativeProxy); + } + + @Override protected void finalize() throws Throwable { try { nDeleteProxy(mNativeProxy); @@ -293,8 +331,53 @@ public class ThreadedRenderer extends HardwareRenderer { } } - /** @hide */ - public static native void postToRenderThread(Runnable runnable); + static void startTrimMemory(int level) { + // TODO + } + + static void endTrimMemory() { + // TODO + } + + private static class AtlasInitializer { + static AtlasInitializer sInstance = new AtlasInitializer(); + + private boolean mInitialized = false; + + private AtlasInitializer() {} + + synchronized void init() { + if (mInitialized) return; + 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) { + nSetAtlas(buffer, map); + mInitialized = true; + } + // 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); + } + } + } + + static native void setupShadersDiskCache(String cacheFile); + + private static native void nSetAtlas(GraphicBuffer buffer, long[] map); private static native long nCreateRootRenderNode(); private static native long nCreateProxy(boolean translucent, long rootRenderNode); @@ -306,11 +389,11 @@ public class ThreadedRenderer extends HardwareRenderer { 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 nSetup(long nativeProxy, int width, int height, + float lightX, float lightY, float lightZ, float lightRadius); private static native void nSetOpaque(long nativeProxy, boolean opaque); - private static native void nSetDisplayListData(long nativeProxy, long displayList, - long newData); - private static native int nSyncAndDrawFrame(long nativeProxy, long frameTimeNanos, + private static native int nSyncAndDrawFrame(long nativeProxy, + long frameTimeNanos, long recordDuration, float density, int dirtyLeft, int dirtyTop, int dirtyRight, int dirtyBottom); private static native void nRunWithGlContext(long nativeProxy, Runnable runnable); private static native void nDestroyCanvasAndSurface(long nativeProxy); @@ -320,7 +403,13 @@ public class ThreadedRenderer extends HardwareRenderer { 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 nPushLayerUpdate(long nativeProxy, long layer); + private static native void nCancelLayerUpdate(long nativeProxy, long layer); + + private static native void nFlushCaches(long nativeProxy, int flushMode); private static native void nFence(long nativeProxy); + private static native void nNotifyFramePending(long nativeProxy); + + private static native void nDumpProfileInfo(long nativeProxy, FileDescriptor fd); } diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index bef96b1..622fa8c 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -670,7 +670,7 @@ import java.util.concurrent.atomic.AtomicInteger; * @attr ref android.R.styleable#View_scrollbarAlwaysDrawHorizontalTrack * @attr ref android.R.styleable#View_scrollbarAlwaysDrawVerticalTrack * @attr ref android.R.styleable#View_stateListAnimator - * @attr ref android.R.styleable#View_sharedElementName + * @attr ref android.R.styleable#View_viewName * @attr ref android.R.styleable#View_soundEffectsEnabled * @attr ref android.R.styleable#View_tag * @attr ref android.R.styleable#View_textAlignment @@ -2774,8 +2774,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, /** * @hide + * + * Makes system ui transparent. + */ + public static final int SYSTEM_UI_TRANSPARENT = 0x00008000; + + /** + * @hide */ - public static final int PUBLIC_STATUS_BAR_VISIBILITY_MASK = 0x0000FFFF; + public static final int PUBLIC_STATUS_BAR_VISIBILITY_MASK = 0x00007FFF; /** * These are the system UI flags that can be cleared by events outside @@ -3185,6 +3192,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private int mBackgroundResource; private boolean mBackgroundSizeChanged; + private String mViewName; + static class ListenerInfo { /** * Listener used to dispatch focus change events. @@ -3997,8 +4006,8 @@ 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)); + case R.styleable.View_viewName: + setViewName(a.getString(attr)); break; case R.styleable.View_nestedScrollingEnabled: setNestedScrollingEnabled(a.getBoolean(attr, false)); @@ -4765,8 +4774,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mAttachInfo.mTreeObserver.dispatchOnGlobalFocusChange(oldFocus, this); } - manageFocusHotspot(true, oldFocus); onFocusChanged(true, direction, previouslyFocusedRect); + manageFocusHotspot(true, oldFocus); refreshDrawableState(); } } @@ -4780,25 +4789,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * @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 (mBackground == null) { + return; + } - if (!focused) { - mBackground.removeHotspot(R.attr.state_focused); - } + 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(x, y); } /** @@ -6743,6 +6750,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the pressed state for this view and provides a touch coordinate for + * animation hinting. + * + * @param pressed Pass true to set the View's internal state to "pressed", + * or false to reverts the View's internal state from a + * previously set "pressed" state. + * @param x The x coordinate of the touch that caused the press + * @param y The y coordinate of the touch that caused the press + */ + private void setPressed(boolean pressed, float x, float y) { + if (pressed) { + setHotspot(x, y); + } + + setPressed(pressed); + } + + /** * Sets the pressed state for this view. * * @see #isClickable() @@ -7145,6 +7170,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, if (viewRootImpl != null) { viewRootImpl.setAccessibilityFocus(this, null); } + Rect rect = (mAttachInfo != null) ? mAttachInfo.mTmpInvalRect : new Rect(); + getDrawingRect(rect); + requestRectangleOnScreen(rect, false); invalidate(); sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED); return true; @@ -8981,7 +9009,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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 @@ -9014,8 +9041,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); + setPressed(true, x, y); } if (!mHasPerformedLongPress) { @@ -9049,8 +9075,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } removeTapCallback(); - } else { - clearHotspot(R.attr.state_pressed); } break; @@ -9076,21 +9100,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away - setHotspot(R.attr.state_pressed, x, y); + setHotspot(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: - setHotspot(R.attr.state_pressed, x, y); + setHotspot(x, y); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { @@ -9112,17 +9135,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, 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); + private void setHotspot(float x, float y) { + if (mBackground != null) { + mBackground.setHotspot(x, y); } } @@ -9163,7 +9178,6 @@ 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); } @@ -9222,6 +9236,30 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Request unbuffered dispatch of the given stream of MotionEvents to this View. + * + * Until this View receives a corresponding {@link MotionEvent#ACTION_UP}, ask that the input + * system not batch {@link MotionEvent}s but instead deliver them as soon as they're + * available. This method should only be called for touch events. + * + * <p class="note">This api is not intended for most applications. Buffered dispatch + * provides many of benefits, and just requesting unbuffered dispatch on most MotionEvent + * streams will not improve your input latency. Side effects include: increased latency, + * jittery scrolls and inability to take advantage of system resampling. Talk to your input + * professional to see if {@link #requestUnbufferedDispatch(MotionEvent)} is right for + * you.</p> + */ + public final void requestUnbufferedDispatch(MotionEvent event) { + final int action = event.getAction(); + if (mAttachInfo == null + || action != MotionEvent.ACTION_DOWN && action != MotionEvent.ACTION_MOVE + || !event.isTouchEvent()) { + return; + } + mAttachInfo.mUnbufferedDispatchRequested = true; + } + + /** * Set flags controlling behavior of this view. * * @param flags Constant indicating the value which should be set @@ -10666,24 +10704,30 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Sets the outline of the view, which defines the shape of the shadow it - * casts. + * Sets the {@link Outline} of the view, which defines the shape of the shadow it + * casts, and enables outline clipping. + * <p> + * By default, a View queries its Outline from its background drawable, via + * {@link Drawable#getOutline(Outline)}. Manually setting the Outline with this method allows + * this behavior to be overridden. * <p> - * If the outline is not set or is null, shadows will be cast from the - * bounds of the View. + * If the outline is {@link Outline#isEmpty()} or is <code>null</code>, + * shadows will not be cast. + * <p> + * Only outlines that return true from {@link Outline#canClip()} may be used for clipping. * * @param outline The new outline of the view. - * Must be {@link android.graphics.Outline#isValid() valid.} + * + * @see #setClipToOutline(boolean) + * @see #getClipToOutline() */ 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; + if (outline == null || outline.isEmpty()) { + if (mOutline != null) { + mOutline.setEmpty(); + } } else { // always copy the path since caller may reuse if (mOutline == null) { @@ -10694,9 +10738,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mRenderNode.setOutline(mOutline); } - // TODO: remove - public final boolean getClipToOutline() { return false; } - public void setClipToOutline(boolean clipToOutline) {} + /** + * Returns whether the Outline should be used to clip the contents of the View. + * <p> + * Note that this flag will only be respected if the View's Outline returns true from + * {@link Outline#canClip()}. + * + * @see #setOutline(Outline) + * @see #setClipToOutline(boolean) + */ + public final boolean getClipToOutline() { + return mRenderNode.getClipToOutline(); + } + + /** + * Sets whether the View's Outline should be used to clip the contents of the View. + * <p> + * Note that this flag will only be respected if the View's Outline returns true from + * {@link Outline#canClip()}. + * + * @see #setOutline(Outline) + * @see #getClipToOutline() + */ + public void setClipToOutline(boolean clipToOutline) { + damageInParent(); + if (getClipToOutline() != clipToOutline) { + mRenderNode.setClipToOutline(clipToOutline); + } + } private void queryOutlineFromBackgroundIfUndefined() { if ((mPrivateFlags3 & PFLAG3_OUTLINE_DEFINED) == 0) { @@ -10705,10 +10774,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mOutline = new Outline(); } else { //invalidate outline, to ensure background calculates it - mOutline.set(null); + mOutline.setEmpty(); } if (mBackground.getOutline(mOutline)) { - if (!mOutline.isValid()) { + if (mOutline.isEmpty()) { throw new IllegalStateException("Background drawable failed to build outline"); } mRenderNode.setOutline(mOutline); @@ -12867,10 +12936,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags &= ~PFLAG_CANCEL_NEXT_UP_EVENT; mPrivateFlags3 &= ~PFLAG3_IS_LAID_OUT; - if (mBackground != null) { - mBackground.clearHotspots(); - } - removeUnsetPressCallback(); removeLongPressCallback(); removePerformClickCallback(); @@ -13528,12 +13593,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } } - // The layer is not valid if the underlying GPU resources cannot be allocated - mHardwareLayer.flushChanges(); - if (!mHardwareLayer.isValid()) { - return null; - } - mHardwareLayer.setLayerPaint(mLayerPaint); RenderNode displayList = mHardwareLayer.startRecording(); updateDisplayListIfDirty(displayList, true); @@ -16132,6 +16191,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Set this view's optical insets. + * + * <p>This method should be treated similarly to setMeasuredDimension and not as a general + * property. Views that compute their own optical insets should call it as part of measurement. + * This method does not request layout. If you are setting optical insets outside of + * measure/layout itself you will want to call requestLayout() yourself. + * </p> + * @hide + */ + public void setOpticalInsets(Insets insets) { + mLayoutInsets = insets; + } + + /** * Changes the selection state of this view. A view can be selected or not. * Note that selection is not the same as focus. Views are typically * selected in the context of an AdapterView like ListView or GridView; @@ -18839,15 +18912,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * 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. + * Adds all Views that have {@link #getViewName()} non-null to namedElements. + * @param namedElements Will contain all Views in the hierarchy having a view name. * @hide */ - public void findSharedElements(Map<String, View> sharedElements) { + public void findNamedViews(Map<String, View> namedElements) { if (getVisibility() == VISIBLE) { - String sharedElementName = getSharedElementName(); - if (sharedElementName != null) { - sharedElements.put(sharedElementName, this); + String viewName = getViewName(); + if (viewName != null) { + namedElements.put(viewName, this); } } } @@ -19215,8 +19288,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Override public void run() { mPrivateFlags &= ~PFLAG_PREPRESSED; - setHotspot(R.attr.state_pressed, x, y); - setPressed(true); + setPressed(true, x, y); checkForLongClick(ViewConfiguration.getTapTimeout()); } } @@ -19247,32 +19319,26 @@ 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. + * Sets the name of the View to be used to identify Views in Transitions. + * 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) + * @param viewName The name of the View to uniquely identify it for Transitions. */ - public void setSharedElementName(String sharedElementName) { - setTagInternal(com.android.internal.R.id.shared_element_name, sharedElementName); + public final void setViewName(String viewName) { + mViewName = viewName; } /** - * 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. + * Returns the name of the View to be used to identify Views in Transitions. + * 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> + * <p>This returns null if the View has not been given a name.</p> * - * @return The name used for this View for cross-Activity transitions or null if - * this View has not been identified as shared. + * @return The name used of the View to be used to identify Views in Transitions or null + * if no name has been given. */ - public String getSharedElementName() { - return (String) getTag(com.android.internal.R.id.shared_element_name); + public String getViewName() { + return mViewName; } /** @@ -19503,7 +19569,6 @@ 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); } } @@ -19727,6 +19792,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback, boolean mInTouchMode; /** + * Indicates whether the view has requested unbuffered input dispatching for the current + * event stream. + */ + boolean mUnbufferedDispatchRequested; + + /** * Indicates that ViewAncestor should trigger a global layout change * the next time it performs a traversal */ diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 4309366..0f40ee7 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -456,6 +456,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // views during a transition when they otherwise would have become gone/invisible private ArrayList<View> mVisibilityChangingChildren; + // Temporary holder of presorted children, only used for + // input/software draw dispatch for correctly Z ordering. + private ArrayList<View> mPreSortedChildren; + // Indicates how many of this container's child subtrees contain transient state @ViewDebug.ExportedProperty(category = "layout") private int mChildCountWithTransientState = 0; @@ -1499,13 +1503,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float y = event.getY(); final int childrenCount = mChildrenCount; if (childrenCount != 0) { - final boolean customChildOrder = isChildrenDrawingOrderEnabled(); + final ArrayList<View> preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; HoverTarget lastHoverTarget = null; for (int i = childrenCount - 1; i >= 0; i--) { - final int childIndex = customChildOrder - ? getChildDrawingOrder(childrenCount, i) : i; - final View child = children[childIndex]; + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; @@ -1572,6 +1578,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager break; } } + if (preorderedList != null) preorderedList.clear(); } } @@ -1778,23 +1785,28 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager // Send the event to the child under the pointer. final int childrenCount = mChildrenCount; if (childrenCount != 0) { - final View[] children = mChildren; final float x = event.getX(); final float y = event.getY(); - final boolean customOrder = isChildrenDrawingOrderEnabled(); + final ArrayList<View> preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { - final int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; - final View child = children[childIndex]; + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; } if (dispatchTransformedGenericPointerEvent(event, child)) { + if (preorderedList != null) preorderedList.clear(); return true; } } + if (preorderedList != null) preorderedList.clear(); } // No child handled the event. Send it to this view group. @@ -1910,13 +1922,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final float y = ev.getY(actionIndex); // Find a child that can receive the event. // Scan children from front to back. + final ArrayList<View> preorderedList = buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; - - final boolean customOrder = isChildrenDrawingOrderEnabled(); for (int i = childrenCount - 1; i >= 0; i--) { - final int childIndex = customOrder ? - getChildDrawingOrder(childrenCount, i) : i; - final View child = children[childIndex]; + final int childIndex = customOrder + ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { continue; @@ -1934,7 +1948,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); - mLastTouchDownIndex = childIndex; + if (preorderedList != null) { + // childIndex points into presorted list, find original index + for (int j = 0; j < childrenCount; j++) { + if (children[childIndex] == mChildren[j]) { + mLastTouchDownIndex = j; + break; + } + } + } else { + mLastTouchDownIndex = childIndex; + } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); @@ -1942,6 +1966,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager break; } } + if (preorderedList != null) preorderedList.clear(); } if (newTouchTarget == null && mFirstTouchTarget != null) { @@ -2301,13 +2326,13 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * 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. + * is not null or if {@link #getViewName()} 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; + return getBackground() != null || getViewName() != null; } } @@ -2318,8 +2343,8 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * 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) + * @see android.app.ActivityOptions#makeSceneTransitionAnimation(android.app.Activity, + * android.util.Pair[]) */ public void setTransitionGroup(boolean isTransitionGroup) { mGroupFlags |= FLAG_IS_TRANSITION_GROUP_SET; @@ -2928,7 +2953,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager */ @Override protected void dispatchDraw(Canvas canvas) { - final int count = mChildrenCount; + final int childrenCount = mChildrenCount; final View[] children = mChildren; int flags = mGroupFlags; @@ -2936,15 +2961,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; final boolean buildCache = !isHardwareAccelerated(); - for (int i = 0; i < count; i++) { + for (int i = 0; i < childrenCount; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { final LayoutParams params = child.getLayoutParams(); - attachLayoutAnimationParameters(child, params, i, count); + attachLayoutAnimationParameters(child, params, i, childrenCount); bindLayoutAnimation(child); if (cache) { child.setDrawingCacheEnabled(true); - if (buildCache) { + if (buildCache) { child.buildDrawingCache(true); } } @@ -2997,21 +3022,22 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager boolean more = false; final long drawingTime = getDrawingTime(); - if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) { - for (int i = 0; i < count; i++) { - final View child = children[i]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { - more |= drawChild(canvas, child, drawingTime); - } - } - } else { - for (int i = 0; i < count; i++) { - final View child = children[getChildDrawingOrder(count, i)]; - if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { - more |= drawChild(canvas, child, drawingTime); - } + + // Only use the preordered list if not HW accelerated, since the HW pipeline will do the + // draw reordering internally + final ArrayList<View> preorderedList = canvas.isHardwareAccelerated() + ? null : buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + for (int i = 0; i < childrenCount; i++) { + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { + more |= drawChild(canvas, child, drawingTime); } } + if (preorderedList != null) preorderedList.clear(); // Draw any disappearing views that have animations if (mDisappearingChildren != null) { @@ -3096,6 +3122,47 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager return i; } + private boolean hasChildWithZ() { + for (int i = 0; i < mChildrenCount; i++) { + if (mChildren[i].getZ() != 0) return true; + } + return false; + } + + /** + * Populates (and returns) mPreSortedChildren with a pre-ordered list of the View's children, + * sorted first by Z, then by child drawing order (if applicable). + * + * Uses a stable, insertion sort which is commonly O(n) for ViewGroups with very few elevated + * children. + */ + private ArrayList<View> buildOrderedChildList() { + final int count = mChildrenCount; + if (count <= 1 || !hasChildWithZ()) return null; + + if (mPreSortedChildren == null) { + mPreSortedChildren = new ArrayList<View>(count); + } else { + mPreSortedChildren.ensureCapacity(count); + } + + final boolean useCustomOrder = isChildrenDrawingOrderEnabled(); + for (int i = 0; i < mChildrenCount; i++) { + // add next child (in child order) to end of list + int childIndex = useCustomOrder ? getChildDrawingOrder(mChildrenCount, i) : i; + View nextChild = mChildren[childIndex]; + float currentZ = nextChild.getZ(); + + // insert ahead of any Views with greater Z + int insertIndex = i; + while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) { + insertIndex--; + } + mPreSortedChildren.add(insertIndex, nextChild); + } + return mPreSortedChildren; + } + private void notifyAnimationListener() { mGroupFlags &= ~FLAG_NOTIFY_ANIMATION_LISTENER; mGroupFlags |= FLAG_ANIMATION_DONE; @@ -5956,15 +6023,15 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager /** @hide */ @Override - public void findSharedElements(Map<String, View> sharedElements) { + public void findNamedViews(Map<String, View> namedElements) { if (getVisibility() != VISIBLE) { return; } - super.findSharedElements(sharedElements); + super.findNamedViews(namedElements); int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); - child.findSharedElements(sharedElements); + child.findNamedViews(namedElements); } } diff --git a/core/java/android/view/ViewPropertyAnimator.java b/core/java/android/view/ViewPropertyAnimator.java index 11d2622..af1de78 100644 --- a/core/java/android/view/ViewPropertyAnimator.java +++ b/core/java/android/view/ViewPropertyAnimator.java @@ -19,6 +19,7 @@ package android.view; import android.animation.Animator; import android.animation.ValueAnimator; import android.animation.TimeInterpolator; +import android.os.Build; import java.util.ArrayList; import java.util.HashMap; @@ -109,6 +110,11 @@ public class ViewPropertyAnimator { private ValueAnimator mTempValueAnimator; /** + * A RenderThread-driven backend that may intercept startAnimation + */ + private ViewPropertyAnimatorRT mRTBackend; + + /** * This listener is the mechanism by which the underlying Animator causes changes to the * properties currently being animated, as well as the cleanup after an animation is * complete. @@ -227,7 +233,7 @@ public class ViewPropertyAnimator { * values are used to calculate the animated value for a given animation fraction * during the animation. */ - private static class NameValuesHolder { + static class NameValuesHolder { int mNameConstant; float mFromValue; float mDeltaValue; @@ -247,6 +253,10 @@ public class ViewPropertyAnimator { ViewPropertyAnimator(View view) { mView = view; view.ensureTransformationInfo(); + // TODO: Disabled because of b/15287046 + //if (view.getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.L) { + // mRTBackend = new ViewPropertyAnimatorRT(view); + //} } /** @@ -371,6 +381,10 @@ public class ViewPropertyAnimator { return this; } + Animator.AnimatorListener getListener() { + return mListener; + } + /** * Sets a listener for update events in the underlying ValueAnimator that runs * the property animations. Note that the underlying animator is animating between @@ -390,6 +404,10 @@ public class ViewPropertyAnimator { return this; } + ValueAnimator.AnimatorUpdateListener getUpdateListener() { + return mUpdateListener; + } + /** * Starts the currently pending property animations immediately. Calling <code>start()</code> * is optional because all animations start automatically at the next opportunity. However, @@ -825,12 +843,22 @@ public class ViewPropertyAnimator { return this; } + boolean hasActions() { + return mPendingSetupAction != null + || mPendingCleanupAction != null + || mPendingOnStartAction != null + || mPendingOnEndAction != null; + } + /** * Starts the underlying Animator for a set of properties. We use a single animator that * simply runs from 0 to 1, and then use that fractional value to set each property * value accordingly. */ private void startAnimation() { + if (mRTBackend != null && mRTBackend.startAnimation(this)) { + return; + } mView.setHasTransientState(true); ValueAnimator animator = ValueAnimator.ofFloat(1.0f); ArrayList<NameValuesHolder> nameValueList = @@ -1115,7 +1143,8 @@ public class ViewPropertyAnimator { // Shouldn't happen, but just to play it safe return; } - boolean useRenderNodeProperties = mView.mRenderNode != null; + + boolean hardwareAccelerated = mView.isHardwareAccelerated(); // alpha requires slightly different treatment than the other (transform) properties. // The logic in setAlpha() is not simply setting mAlpha, plus the invalidation @@ -1123,13 +1152,13 @@ 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 (!useRenderNodeProperties) { + if (!hardwareAccelerated) { mView.invalidateParentCaches(); } float fraction = animation.getAnimatedFraction(); int propertyMask = propertyBundle.mPropertyMask; if ((propertyMask & TRANSFORM_MASK) != 0) { - mView.invalidateViewProperty(false, false); + mView.invalidateViewProperty(hardwareAccelerated, false); } ArrayList<NameValuesHolder> valueList = propertyBundle.mNameValuesHolder; if (valueList != null) { @@ -1145,7 +1174,7 @@ public class ViewPropertyAnimator { } } if ((propertyMask & TRANSFORM_MASK) != 0) { - if (!useRenderNodeProperties) { + if (!hardwareAccelerated) { mView.mPrivateFlags |= View.PFLAG_DRAWN; // force another invalidation } } diff --git a/core/java/android/view/ViewPropertyAnimatorRT.java b/core/java/android/view/ViewPropertyAnimatorRT.java new file mode 100644 index 0000000..709efdb --- /dev/null +++ b/core/java/android/view/ViewPropertyAnimatorRT.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.animation.TimeInterpolator; +import android.view.ViewPropertyAnimator.NameValuesHolder; + +import com.android.internal.view.animation.FallbackLUTInterpolator; + +import java.util.ArrayList; + + +/** + * This is a RenderThread driven backend for ViewPropertyAnimator. + */ +class ViewPropertyAnimatorRT { + + private final View mView; + + private RenderNodeAnimator mAnimators[] = new RenderNodeAnimator[RenderNodeAnimator.LAST_VALUE + 1]; + + ViewPropertyAnimatorRT(View view) { + mView = view; + } + + /** + * @return true if ViewPropertyAnimatorRT handled the animation, + * false if ViewPropertyAnimator needs to handle it + */ + public boolean startAnimation(ViewPropertyAnimator parent) { + cancelAnimators(parent.mPendingAnimations); + if (!canHandleAnimator(parent)) { + return false; + } + doStartAnimation(parent); + return true; + } + + private void doStartAnimation(ViewPropertyAnimator parent) { + int size = parent.mPendingAnimations.size(); + + long startDelay = parent.getStartDelay(); + long duration = parent.getDuration(); + TimeInterpolator interpolator = parent.getInterpolator(); + if (!RenderNodeAnimator.isNativeInterpolator(interpolator)) { + interpolator = new FallbackLUTInterpolator(interpolator, duration); + } + for (int i = 0; i < size; i++) { + NameValuesHolder holder = parent.mPendingAnimations.get(i); + int property = RenderNodeAnimator.mapViewPropertyToRenderProperty(holder.mNameConstant); + + RenderNodeAnimator animator = new RenderNodeAnimator(property, holder.mFromValue + holder.mDeltaValue); + animator.setStartDelay(startDelay); + animator.setDuration(duration); + animator.setInterpolator(interpolator); + animator.setTarget(mView); + animator.start(); + } + + parent.mPendingAnimations.clear(); + } + + private boolean canHandleAnimator(ViewPropertyAnimator parent) { + // TODO: Can we eliminate this entirely? + // If RenderNode.animatorProperties() can be toggled to point at staging + // instead then RNA can be used as the animators for software as well + // as the updateListener fallback paths. If this can be toggled + // at the top level somehow, combined with requiresUiRedraw, we could + // ensure that RT does not self-animate, allowing for safe driving of + // the animators from the UI thread using the same mechanisms + // ViewPropertyAnimator does, just with everything sitting on a single + // animator subsystem instead of multiple. + + if (parent.getUpdateListener() != null) { + return false; + } + if (parent.getListener() != null) { + // TODO support + return false; + } + if (!mView.isHardwareAccelerated()) { + // TODO handle this maybe? + return false; + } + if (parent.hasActions()) { + return false; + } + // Here goes nothing... + return true; + } + + private void cancelAnimators(ArrayList<NameValuesHolder> mPendingAnimations) { + int size = mPendingAnimations.size(); + for (int i = 0; i < size; i++) { + NameValuesHolder holder = mPendingAnimations.get(i); + int property = RenderNodeAnimator.mapViewPropertyToRenderProperty(holder.mNameConstant); + if (mAnimators[property] != null) { + mAnimators[property].cancel(); + mAnimators[property] = null; + } + } + } + +} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9b09d85..f3d1e3c 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -47,7 +47,6 @@ import android.os.Handler; import android.os.Looper; import android.os.Message; import android.os.ParcelFileDescriptor; -import android.os.PowerManager; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -111,6 +110,7 @@ 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_STAGES = false || LOCAL_LOGV; /** * Set this system property to true to force the view hierarchy to render @@ -230,6 +230,7 @@ public final class ViewRootImpl implements ViewParent, QueuedInputEvent mPendingInputEventTail; int mPendingInputEventCount; boolean mProcessInputEventsScheduled; + boolean mUnbufferedInputDispatch; String mPendingInputEventQueueLengthCounterName = "pq"; InputStage mFirstInputStage; @@ -668,6 +669,11 @@ public final class ViewRootImpl implements ViewParent, public void detachFunctor(long functor) { // TODO: Make the resize buffer some other way to not need this block mBlockResizeBuffer = true; + if (mAttachInfo.mHardwareRenderer != null) { + // Fence so that any pending invokeFunctor() messages will be processed + // before we return from detachFunctor. + mAttachInfo.mHardwareRenderer.fence(); + } } public boolean invokeFunctor(long functor, boolean waitForCompletion) { @@ -710,17 +716,6 @@ public final class ViewRootImpl implements ViewParent, if (!HardwareRenderer.sRendererDisabled || (HardwareRenderer.sSystemRendererDisabled && forceHwAccelerated)) { - if (!HardwareRenderer.sUseRenderThread) { - // TODO: Delete - // Don't enable hardware acceleration when we're not on the main thread - if (!HardwareRenderer.sSystemRendererDisabled && - Looper.getMainLooper() != Looper.myLooper()) { - Log.w(HardwareRenderer.LOG_TAG, "Attempting to initialize hardware " - + "acceleration outside of the main thread, aborting"); - return; - } - } - if (mAttachInfo.mHardwareRenderer != null) { mAttachInfo.mHardwareRenderer.destroy(true); } @@ -994,13 +989,27 @@ public final class ViewRootImpl implements ViewParent, } } + /** + * Notifies the HardwareRenderer that a new frame will be coming soon. + * Currently only {@link ThreadedRenderer} cares about this, and uses + * this knowledge to adjust the scheduling of off-thread animations + */ + void notifyRendererOfFramePending() { + if (mAttachInfo.mHardwareRenderer != null) { + mAttachInfo.mHardwareRenderer.notifyFramePending(); + } + } + void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); - scheduleConsumeBatchedInput(); + if (!mUnbufferedInputDispatch) { + scheduleConsumeBatchedInput(); + } + notifyRendererOfFramePending(); } } @@ -1708,7 +1717,8 @@ public final class ViewRootImpl implements ViewParent, if (hwInitialized || mWidth != mAttachInfo.mHardwareRenderer.getWidth() || mHeight != mAttachInfo.mHardwareRenderer.getHeight()) { - mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight); + mAttachInfo.mHardwareRenderer.setup(mWidth, mHeight, + mAttachInfo.mRootView.getResources().getDisplayMetrics()); if (!hwInitialized) { mAttachInfo.mHardwareRenderer.invalidate(mSurface); mFullRedrawNeeded = true; @@ -2447,7 +2457,7 @@ public final class ViewRootImpl implements ViewParent, try { attachInfo.mHardwareRenderer.initializeIfNeeded(mWidth, mHeight, - mSurface); + mSurface, attachInfo.mRootView.getResources().getDisplayMetrics()); } catch (OutOfResourcesException e) { handleOutOfResourcesException(e); return; @@ -2598,7 +2608,7 @@ public final class ViewRootImpl implements ViewParent, } final AccessibilityNodeProvider provider = host.getAccessibilityNodeProvider(); - final Rect bounds = mView.mAttachInfo.mTmpInvalRect; + final Rect bounds = mAttachInfo.mTmpInvalRect; if (provider == null) { host.getBoundsOnScreen(bounds); } else if (mAccessibilityFocusedVirtualView != null) { @@ -3145,7 +3155,8 @@ public final class ViewRootImpl implements ViewParent, mFullRedrawNeeded = true; try { mAttachInfo.mHardwareRenderer.initializeIfNeeded( - mWidth, mHeight, mSurface); + mWidth, mHeight, mSurface, + mAttachInfo.mRootView.getResources().getDisplayMetrics()); } catch (OutOfResourcesException e) { Log.e(TAG, "OutOfResourcesException locking surface", e); try { @@ -3481,6 +3492,9 @@ public final class ViewRootImpl implements ViewParent, * Called when an event is being delivered to the next stage. */ protected void onDeliverToNext(QueuedInputEvent q) { + if (DEBUG_INPUT_STAGES) { + Log.v(TAG, "Done with " + getClass().getSimpleName() + ". " + q); + } if (mNext != null) { mNext.deliver(q); } else { @@ -3876,6 +3890,18 @@ public final class ViewRootImpl implements ViewParent, } } + @Override + protected void onDeliverToNext(QueuedInputEvent q) { + if (mUnbufferedInputDispatch + && q.mEvent instanceof MotionEvent + && ((MotionEvent)q.mEvent).isTouchEvent() + && isTerminalInputEvent(q.mEvent)) { + mUnbufferedInputDispatch = false; + scheduleConsumeBatchedInput(); + } + super.onDeliverToNext(q); + } + private int processKeyEvent(QueuedInputEvent q) { final KeyEvent event = (KeyEvent)q.mEvent; @@ -3988,10 +4014,15 @@ public final class ViewRootImpl implements ViewParent, private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; - if (mView.dispatchPointerEvent(event)) { - return FINISH_HANDLED; + mAttachInfo.mUnbufferedDispatchRequested = false; + boolean handled = mView.dispatchPointerEvent(event); + if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) { + mUnbufferedInputDispatch = true; + if (mConsumeBatchedInputScheduled) { + scheduleConsumeBatchedInputImmediately(); + } } - return FORWARD; + return handled ? FINISH_HANDLED : FORWARD; } private int processTrackballEvent(QueuedInputEvent q) { @@ -5256,6 +5287,8 @@ public final class ViewRootImpl implements ViewParent, writer.print(" mRemoved="); writer.println(mRemoved); writer.print(innerPrefix); writer.print("mConsumeBatchedInputScheduled="); writer.println(mConsumeBatchedInputScheduled); + writer.print(innerPrefix); writer.print("mConsumeBatchedInputImmediatelyScheduled="); + writer.println(mConsumeBatchedInputImmediatelyScheduled); writer.print(innerPrefix); writer.print("mPendingInputEventCount="); writer.println(mPendingInputEventCount); writer.print(innerPrefix); writer.print("mProcessInputEventsScheduled="); @@ -5307,7 +5340,7 @@ public final class ViewRootImpl implements ViewParent, RenderNode renderNode = view.mRenderNode; info[0]++; if (renderNode != null) { - info[1] += 0; /* TODO: Memory used by RenderNodes (properties + DisplayLists) */ + info[1] += renderNode.getDebugSize(); } if (view instanceof ViewGroup) { @@ -5515,6 +5548,37 @@ public final class ViewRootImpl implements ViewParent, return false; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("QueuedInputEvent{flags="); + boolean hasPrevious = false; + hasPrevious = flagToString("DELIVER_POST_IME", FLAG_DELIVER_POST_IME, hasPrevious, sb); + hasPrevious = flagToString("DEFERRED", FLAG_DEFERRED, hasPrevious, sb); + hasPrevious = flagToString("FINISHED", FLAG_FINISHED, hasPrevious, sb); + hasPrevious = flagToString("FINISHED_HANDLED", FLAG_FINISHED_HANDLED, hasPrevious, sb); + hasPrevious = flagToString("RESYNTHESIZED", FLAG_RESYNTHESIZED, hasPrevious, sb); + hasPrevious = flagToString("UNHANDLED", FLAG_UNHANDLED, hasPrevious, sb); + if (!hasPrevious) { + sb.append("0"); + } + sb.append(", hasNextQueuedEvent=" + (mEvent != null ? "true" : "false")); + sb.append(", hasInputEventReceiver=" + (mReceiver != null ? "true" : "false")); + sb.append(", mEvent=" + mEvent + "}"); + return sb.toString(); + } + + private boolean flagToString(String name, int flag, + boolean hasPrevious, StringBuilder sb) { + if ((mFlags & flag) != 0) { + if (hasPrevious) { + sb.append("|"); + } + sb.append(name); + return true; + } + return hasPrevious; + } } private QueuedInputEvent obtainQueuedInputEvent(InputEvent event, @@ -5635,6 +5699,7 @@ public final class ViewRootImpl implements ViewParent, 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); @@ -5674,15 +5739,25 @@ public final class ViewRootImpl implements ViewParent, } } + void scheduleConsumeBatchedInputImmediately() { + if (!mConsumeBatchedInputImmediatelyScheduled) { + unscheduleConsumeBatchedInput(); + mConsumeBatchedInputImmediatelyScheduled = true; + mHandler.post(mConsumeBatchedInputImmediatelyRunnable); + } + } + void doConsumeBatchedInput(long frameTimeNanos) { if (mConsumeBatchedInputScheduled) { mConsumeBatchedInputScheduled = false; if (mInputEventReceiver != null) { - if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos)) { + if (mInputEventReceiver.consumeBatchedInputEvents(frameTimeNanos) + && frameTimeNanos != -1) { // If we consumed a batch here, we want to go ahead and schedule the // consumption of batched input events on the next frame. Otherwise, we would // wait until we have more input events pending and might get starved by other - // things occurring in the process. + // things occurring in the process. If the frame time is -1, however, then + // we're in a non-batching mode, so there's no need to schedule this. scheduleConsumeBatchedInput(); } } @@ -5710,7 +5785,11 @@ public final class ViewRootImpl implements ViewParent, @Override public void onBatchedInputEventPending() { - scheduleConsumeBatchedInput(); + if (mUnbufferedInputDispatch) { + super.onBatchedInputEventPending(); + } else { + scheduleConsumeBatchedInput(); + } } @Override @@ -5731,6 +5810,16 @@ public final class ViewRootImpl implements ViewParent, new ConsumeBatchedInputRunnable(); boolean mConsumeBatchedInputScheduled; + final class ConsumeBatchedInputImmediatelyRunnable implements Runnable { + @Override + public void run() { + doConsumeBatchedInput(-1); + } + } + final ConsumeBatchedInputImmediatelyRunnable mConsumeBatchedInputImmediatelyRunnable = + new ConsumeBatchedInputImmediatelyRunnable(); + boolean mConsumeBatchedInputImmediatelyScheduled; + final class InvalidateOnAnimationRunnable implements Runnable { private boolean mPosted; private final ArrayList<View> mViews = new ArrayList<View>(); diff --git a/core/java/android/view/VolumePanel.java b/core/java/android/view/VolumePanel.java deleted file mode 100644 index 4730e59..0000000 --- a/core/java/android/view/VolumePanel.java +++ /dev/null @@ -1,1076 +0,0 @@ -/* - * Copyright (C) 2007 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR 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 com.android.internal.R; - -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface.OnDismissListener; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; -import android.content.IntentFilter; -import android.content.res.Resources; -import android.media.AudioManager; -import android.media.AudioService; -import android.media.AudioSystem; -import android.media.RingtoneManager; -import android.media.ToneGenerator; -import android.media.VolumeController; -import android.net.Uri; -import android.os.Handler; -import android.os.Message; -import android.os.Vibrator; -import android.util.Log; -import android.view.WindowManager.LayoutParams; -import android.widget.ImageView; -import android.widget.SeekBar; -import android.widget.SeekBar.OnSeekBarChangeListener; - -import java.util.HashMap; - -/** - * Handle the volume up and down keys. - * - * This code really should be moved elsewhere. - * - * Seriously, it really really should be moved elsewhere. This is used by - * android.media.AudioService, which actually runs in the system process, to - * show the volume dialog when the user changes the volume. What a mess. - * - * @hide - */ -public class VolumePanel extends Handler implements VolumeController { - private static final String TAG = VolumePanel.class.getSimpleName(); - private static boolean LOGD = false; - - /** - * The delay before playing a sound. This small period exists so the user - * can press another key (non-volume keys, too) to have it NOT be audible. - * <p> - * PhoneWindow will implement this part. - */ - public static final int PLAY_SOUND_DELAY = 300; - - /** - * The delay before vibrating. This small period exists so if the user is - * moving to silent mode, it will not emit a short vibrate (it normally - * would since vibrate is between normal mode and silent mode using hardware - * keys). - */ - public static final int VIBRATE_DELAY = 300; - - private static final int VIBRATE_DURATION = 300; - private static final int BEEP_DURATION = 150; - private static final int MAX_VOLUME = 100; - private static final int FREE_DELAY = 10000; - private static final int TIMEOUT_DELAY = 3000; - - private static final int MSG_VOLUME_CHANGED = 0; - private static final int MSG_FREE_RESOURCES = 1; - private static final int MSG_PLAY_SOUND = 2; - private static final int MSG_STOP_SOUNDS = 3; - private static final int MSG_VIBRATE = 4; - private static final int MSG_TIMEOUT = 5; - private static final int MSG_RINGER_MODE_CHANGED = 6; - private static final int MSG_MUTE_CHANGED = 7; - private static final int MSG_REMOTE_VOLUME_CHANGED = 8; - private static final int MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN = 9; - private static final int MSG_SLIDER_VISIBILITY_CHANGED = 10; - private static final int MSG_DISPLAY_SAFE_VOLUME_WARNING = 11; - - // Pseudo stream type for master volume - private static final int STREAM_MASTER = -100; - // Pseudo stream type for remote volume is defined in AudioService.STREAM_REMOTE_MUSIC - - protected Context mContext; - private AudioManager mAudioManager; - protected AudioService mAudioService; - private boolean mRingIsSilent; - private boolean mShowCombinedVolumes; - private boolean mVoiceCapable; - - // True if we want to play tones on the system stream when the master stream is specified. - private final boolean mPlayMasterStreamTones; - - /** Dialog containing all the sliders */ - private final Dialog mDialog; - /** Dialog's content view */ - private final View mView; - - /** The visible portion of the volume overlay */ - private final ViewGroup mPanel; - /** Contains the sliders and their touchable icons */ - private final ViewGroup mSliderGroup; - /** The button that expands the dialog to show all sliders */ - private final View mMoreButton; - /** Dummy divider icon that needs to vanish with the more button */ - private final View mDivider; - - /** Currently active stream that shows up at the top of the list of sliders */ - private int mActiveStreamType = -1; - /** All the slider controls mapped by stream type */ - private HashMap<Integer,StreamControl> mStreamControls; - - private enum StreamResources { - BluetoothSCOStream(AudioManager.STREAM_BLUETOOTH_SCO, - R.string.volume_icon_description_bluetooth, - R.drawable.ic_audio_bt, - R.drawable.ic_audio_bt, - false), - RingerStream(AudioManager.STREAM_RING, - R.string.volume_icon_description_ringer, - R.drawable.ic_audio_ring_notif, - R.drawable.ic_audio_ring_notif_mute, - false), - VoiceStream(AudioManager.STREAM_VOICE_CALL, - R.string.volume_icon_description_incall, - R.drawable.ic_audio_phone, - R.drawable.ic_audio_phone, - false), - AlarmStream(AudioManager.STREAM_ALARM, - R.string.volume_alarm, - R.drawable.ic_audio_alarm, - R.drawable.ic_audio_alarm_mute, - false), - MediaStream(AudioManager.STREAM_MUSIC, - R.string.volume_icon_description_media, - R.drawable.ic_audio_vol, - R.drawable.ic_audio_vol_mute, - true), - NotificationStream(AudioManager.STREAM_NOTIFICATION, - R.string.volume_icon_description_notification, - R.drawable.ic_audio_notification, - R.drawable.ic_audio_notification_mute, - true), - // for now, use media resources for master volume - MasterStream(STREAM_MASTER, - R.string.volume_icon_description_media, //FIXME should have its own description - R.drawable.ic_audio_vol, - R.drawable.ic_audio_vol_mute, - false), - RemoteStream(AudioService.STREAM_REMOTE_MUSIC, - R.string.volume_icon_description_media, //FIXME should have its own description - R.drawable.ic_media_route_on_holo_dark, - R.drawable.ic_media_route_disabled_holo_dark, - false);// will be dynamically updated - - int streamType; - int descRes; - int iconRes; - int iconMuteRes; - // RING, VOICE_CALL & BLUETOOTH_SCO are hidden unless explicitly requested - boolean show; - - StreamResources(int streamType, int descRes, int iconRes, int iconMuteRes, boolean show) { - this.streamType = streamType; - this.descRes = descRes; - this.iconRes = iconRes; - this.iconMuteRes = iconMuteRes; - this.show = show; - } - } - - // List of stream types and their order - private static final StreamResources[] STREAMS = { - StreamResources.BluetoothSCOStream, - StreamResources.RingerStream, - StreamResources.VoiceStream, - StreamResources.MediaStream, - StreamResources.NotificationStream, - StreamResources.AlarmStream, - StreamResources.MasterStream, - StreamResources.RemoteStream - }; - - /** Object that contains data for each slider */ - private class StreamControl { - int streamType; - ViewGroup group; - ImageView icon; - SeekBar seekbarView; - int iconRes; - int iconMuteRes; - } - - // Synchronize when accessing this - private ToneGenerator mToneGenerators[]; - private Vibrator mVibrator; - - private static AlertDialog sConfirmSafeVolumeDialog; - private static Object sConfirmSafeVolumeLock = new Object(); - - private static class WarningDialogReceiver extends BroadcastReceiver - implements DialogInterface.OnDismissListener { - private final Context mContext; - private final Dialog mDialog; - private final VolumePanel mVolumePanel; - - WarningDialogReceiver(Context context, Dialog dialog, VolumePanel volumePanel) { - mContext = context; - mDialog = dialog; - mVolumePanel = volumePanel; - IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); - context.registerReceiver(this, filter); - } - - @Override - public void onReceive(Context context, Intent intent) { - mDialog.cancel(); - cleanUp(); - } - - @Override - public void onDismiss(DialogInterface unused) { - mContext.unregisterReceiver(this); - cleanUp(); - } - - private void cleanUp() { - synchronized (sConfirmSafeVolumeLock) { - sConfirmSafeVolumeDialog = null; - } - mVolumePanel.forceTimeout(); - mVolumePanel.updateStates(); - } - } - - - 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 - 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]; - streamRes.show = (streamRes.streamType == STREAM_MASTER); - } - } - - mDialog = new Dialog(context) { - @Override - public boolean onTouchEvent(MotionEvent event) { - if (isShowing() && event.getAction() == MotionEvent.ACTION_OUTSIDE && - sConfirmSafeVolumeDialog == null) { - forceTimeout(); - return true; - } - return false; - } - }; - - // Change some window properties - final Window window = mDialog.getWindow(); - final LayoutParams lp = window.getAttributes(); - lp.token = null; - // Offset from the top - lp.y = res.getDimensionPixelOffset(R.dimen.volume_panel_top); - lp.type = LayoutParams.TYPE_VOLUME_OVERLAY; - lp.windowAnimations = R.style.Animation_VolumePanel; - window.setAttributes(lp); - 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); - - 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 (!mShowCombinedVolumes) { - mMoreButton.setVisibility(View.GONE); - mDivider.setVisibility(View.GONE); - } else { - mMoreButton.setOnClickListener(mClickListener); - } - - final boolean masterVolumeOnly = res.getBoolean(R.bool.config_useMasterVolume); - final boolean masterVolumeKeySounds = res.getBoolean(R.bool.config_useVolumeKeySounds); - mPlayMasterStreamTones = masterVolumeOnly && masterVolumeKeySounds; - - listenToRingerMode(); - } - - public void setLayoutDirection(int layoutDirection) { - mPanel.setLayoutDirection(layoutDirection); - updateStates(); - } - - private void listenToRingerMode() { - 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(); - - if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) { - removeMessages(MSG_RINGER_MODE_CHANGED); - sendMessage(obtainMessage(MSG_RINGER_MODE_CHANGED)); - } - } - }, filter); - } - - private boolean isMuted(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.isMasterMute(); - } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { - return (mAudioService.getRemoteStreamVolume() <= 0); - } else { - return mAudioManager.isStreamMute(streamType); - } - } - - private int getStreamMaxVolume(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.getMasterMaxVolume(); - } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { - return mAudioService.getRemoteStreamMaxVolume(); - } else { - return mAudioManager.getStreamMaxVolume(streamType); - } - } - - private int getStreamVolume(int streamType) { - if (streamType == STREAM_MASTER) { - return mAudioManager.getMasterVolume(); - } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { - return mAudioService.getRemoteStreamVolume(); - } else { - return mAudioManager.getStreamVolume(streamType); - } - } - - private void setStreamVolume(int streamType, int index, int flags) { - if (streamType == STREAM_MASTER) { - mAudioManager.setMasterVolume(index, flags); - } else if (streamType == AudioService.STREAM_REMOTE_MUSIC) { - mAudioService.setRemoteStreamVolume(index); - } else { - mAudioManager.setStreamVolume(streamType, index, flags); - } - } - - private void createSliders() { - final Resources res = mContext.getResources(); - final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( - Context.LAYOUT_INFLATER_SERVICE); - - mStreamControls = new HashMap<Integer, StreamControl>(STREAMS.length); - - for (int i = 0; i < STREAMS.length; i++) { - StreamResources streamRes = STREAMS[i]; - - final int streamType = streamRes.streamType; - if (mVoiceCapable && streamRes == StreamResources.NotificationStream) { - streamRes = StreamResources.RingerStream; - } - - final StreamControl sc = new StreamControl(); - sc.streamType = streamType; - sc.group = (ViewGroup) inflater.inflate(R.layout.volume_adjust_item, null); - sc.group.setTag(sc); - sc.icon = (ImageView) sc.group.findViewById(R.id.stream_icon); - sc.icon.setTag(sc); - sc.icon.setContentDescription(res.getString(streamRes.descRes)); - sc.iconRes = streamRes.iconRes; - sc.iconMuteRes = streamRes.iconMuteRes; - sc.icon.setImageResource(sc.iconRes); - sc.seekbarView = (SeekBar) sc.group.findViewById(R.id.seekbar); - final int plusOne = (streamType == AudioSystem.STREAM_BLUETOOTH_SCO || - streamType == AudioSystem.STREAM_VOICE_CALL) ? 1 : 0; - sc.seekbarView.setMax(getStreamMaxVolume(streamType) + plusOne); - sc.seekbarView.setOnSeekBarChangeListener(mSeekListener); - sc.seekbarView.setTag(sc); - mStreamControls.put(streamType, sc); - } - } - - private void reorderSliders(int activeStreamType) { - mSliderGroup.removeAllViews(); - - final StreamControl active = mStreamControls.get(activeStreamType); - if (active == null) { - Log.e("VolumePanel", "Missing stream type! - " + activeStreamType); - mActiveStreamType = -1; - } else { - mSliderGroup.addView(active.group); - mActiveStreamType = activeStreamType; - active.group.setVisibility(View.VISIBLE); - updateSlider(active); - } - - addOtherVolumes(); - } - - private void addOtherVolumes() { - if (!mShowCombinedVolumes) return; - - for (int i = 0; i < STREAMS.length; i++) { - // Skip the phone specific ones and the active one - final int streamType = STREAMS[i].streamType; - if (!STREAMS[i].show || streamType == mActiveStreamType) { - continue; - } - StreamControl sc = mStreamControls.get(streamType); - mSliderGroup.addView(sc.group); - updateSlider(sc); - } - } - - /** Update the mute and progress state of a slider */ - private void updateSlider(StreamControl sc) { - sc.seekbarView.setProgress(getStreamVolume(sc.streamType)); - final boolean muted = isMuted(sc.streamType); - // Force reloading the image resource - sc.icon.setImageDrawable(null); - sc.icon.setImageResource(muted ? sc.iconMuteRes : sc.iconRes); - if (((sc.streamType == AudioManager.STREAM_RING) || - (sc.streamType == AudioManager.STREAM_NOTIFICATION)) && - mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { - sc.icon.setImageResource(R.drawable.ic_audio_ring_notif_vibrate); - } - if (sc.streamType == AudioService.STREAM_REMOTE_MUSIC) { - // never disable touch interactions for remote playback, the muting is not tied to - // the state of the phone. - sc.seekbarView.setEnabled(true); - } else if ((sc.streamType != mAudioManager.getMasterStreamType() && muted) || - (sConfirmSafeVolumeDialog != null)) { - sc.seekbarView.setEnabled(false); - } else { - sc.seekbarView.setEnabled(true); - } - } - - private void expand() { - final int count = mSliderGroup.getChildCount(); - for (int i = 0; i < count; i++) { - mSliderGroup.getChildAt(i).setVisibility(View.VISIBLE); - } - mMoreButton.setVisibility(View.INVISIBLE); - mDivider.setVisibility(View.INVISIBLE); - } - - private void collapse() { - mMoreButton.setVisibility(View.VISIBLE); - mDivider.setVisibility(View.VISIBLE); - final int count = mSliderGroup.getChildCount(); - for (int i = 1; i < count; i++) { - mSliderGroup.getChildAt(i).setVisibility(View.GONE); - } - } - - public void updateStates() { - final int count = mSliderGroup.getChildCount(); - for (int i = 0; i < count; i++) { - StreamControl sc = (StreamControl) mSliderGroup.getChildAt(i).getTag(); - updateSlider(sc); - } - } - - public void postVolumeChanged(int streamType, int flags) { - if (hasMessages(MSG_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - obtainMessage(MSG_VOLUME_CHANGED, streamType, flags).sendToTarget(); - } - - @Override - public void postRemoteVolumeChanged(int streamType, int flags) { - if (hasMessages(MSG_REMOTE_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - 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(); - } - - /** - * Called by AudioService when it has received new remote playback information that - * would affect the VolumePanel display (mainly volumes). The difference with - * {@link #postRemoteVolumeChanged(int, int)} is that the handling of the posted message - * (MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN) will only update the volume slider if it is being - * displayed. - * This special code path is due to the fact that remote volume updates arrive to AudioService - * asynchronously. So after AudioService has sent the volume update (which should be treated - * 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 - // late and shouldn't warrant the panel to be displayed longer - obtainMessage(MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN).sendToTarget(); - } - - public void postMasterVolumeChanged(int flags) { - postVolumeChanged(STREAM_MASTER, flags); - } - - public void postMuteChanged(int streamType, int flags) { - if (hasMessages(MSG_VOLUME_CHANGED)) return; - synchronized (this) { - if (mStreamControls == null) { - createSliders(); - } - } - removeMessages(MSG_FREE_RESOURCES); - obtainMessage(MSG_MUTE_CHANGED, streamType, flags).sendToTarget(); - } - - public void postMasterMuteChanged(int flags) { - postMuteChanged(STREAM_MASTER, flags); - } - - public void postDisplaySafeVolumeWarning(int flags) { - if (hasMessages(MSG_DISPLAY_SAFE_VOLUME_WARNING)) return; - obtainMessage(MSG_DISPLAY_SAFE_VOLUME_WARNING, flags, 0).sendToTarget(); - } - - /** - * Override this if you have other work to do when the volume changes (for - * example, vibrating, playing a sound, etc.). Make sure to call through to - * the superclass implementation. - */ - protected void onVolumeChanged(int streamType, int flags) { - - if (LOGD) Log.d(TAG, "onVolumeChanged(streamType: " + streamType + ", flags: " + flags + ")"); - - if ((flags & AudioManager.FLAG_SHOW_UI) != 0) { - synchronized (this) { - if (mActiveStreamType != streamType) { - reorderSliders(streamType); - } - onShowVolumeChanged(streamType, flags); - } - } - - if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { - removeMessages(MSG_PLAY_SOUND); - sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); - } - - if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { - removeMessages(MSG_PLAY_SOUND); - removeMessages(MSG_VIBRATE); - onStopSounds(); - } - - removeMessages(MSG_FREE_RESOURCES); - sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); - } - - protected void onMuteChanged(int streamType, int flags) { - - if (LOGD) Log.d(TAG, "onMuteChanged(streamType: " + streamType + ", flags: " + flags + ")"); - - StreamControl sc = mStreamControls.get(streamType); - if (sc != null) { - sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); - } - - onVolumeChanged(streamType, flags); - } - - protected void onShowVolumeChanged(int streamType, int flags) { - int index = getStreamVolume(streamType); - - mRingIsSilent = false; - - if (LOGD) { - Log.d(TAG, "onShowVolumeChanged(streamType: " + streamType - + ", flags: " + flags + "), index: " + index); - } - - // get max volume for progress bar - - int max = getStreamMaxVolume(streamType); - - switch (streamType) { - - case AudioManager.STREAM_RING: { -// setRingerIcon(); - Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( - mContext, RingtoneManager.TYPE_RINGTONE); - if (ringuri == null) { - mRingIsSilent = true; - } - break; - } - - case AudioManager.STREAM_MUSIC: { - // Special case for when Bluetooth is active for music - if ((mAudioManager.getDevicesForStream(AudioManager.STREAM_MUSIC) & - (AudioManager.DEVICE_OUT_BLUETOOTH_A2DP | - AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES | - AudioManager.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER)) != 0) { - setMusicIcon(R.drawable.ic_audio_bt, R.drawable.ic_audio_bt_mute); - } else { - setMusicIcon(R.drawable.ic_audio_vol, R.drawable.ic_audio_vol_mute); - } - break; - } - - case AudioManager.STREAM_VOICE_CALL: { - /* - * For in-call voice call volume, there is no inaudible volume. - * Rescale the UI control so the progress bar doesn't go all - * the way to zero and don't show the mute icon. - */ - index++; - max++; - break; - } - - case AudioManager.STREAM_ALARM: { - break; - } - - case AudioManager.STREAM_NOTIFICATION: { - Uri ringuri = RingtoneManager.getActualDefaultRingtoneUri( - mContext, RingtoneManager.TYPE_NOTIFICATION); - if (ringuri == null) { - mRingIsSilent = true; - } - break; - } - - case AudioManager.STREAM_BLUETOOTH_SCO: { - /* - * For in-call voice call volume, there is no inaudible volume. - * Rescale the UI control so the progress bar doesn't go all - * the way to zero and don't show the mute icon. - */ - index++; - max++; - break; - } - - case AudioService.STREAM_REMOTE_MUSIC: { - if (LOGD) { Log.d(TAG, "showing remote volume "+index+" over "+ max); } - break; - } - } - - StreamControl sc = mStreamControls.get(streamType); - if (sc != null) { - if (sc.seekbarView.getMax() != max) { - sc.seekbarView.setMax(max); - } - - sc.seekbarView.setProgress(index); - if (((flags & AudioManager.FLAG_FIXED_VOLUME) != 0) || - (streamType != mAudioManager.getMasterStreamType() && - streamType != AudioService.STREAM_REMOTE_MUSIC && - isMuted(streamType)) || - sConfirmSafeVolumeDialog != null) { - sc.seekbarView.setEnabled(false); - } else { - sc.seekbarView.setEnabled(true); - } - } - - if (!mDialog.isShowing()) { - 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); - - // Showing dialog - use collapsed state - if (mShowCombinedVolumes) { - collapse(); - } - mDialog.show(); - } - - // Do a little vibrate if applicable (only when going into vibrate mode) - if ((streamType != AudioService.STREAM_REMOTE_MUSIC) && - ((flags & AudioManager.FLAG_VIBRATE) != 0) && - mAudioService.isStreamAffectedByRingerMode(streamType) && - mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE) { - sendMessageDelayed(obtainMessage(MSG_VIBRATE), VIBRATE_DELAY); - } - } - - protected void onPlaySound(int streamType, int flags) { - - if (hasMessages(MSG_STOP_SOUNDS)) { - removeMessages(MSG_STOP_SOUNDS); - // Force stop right now - onStopSounds(); - } - - synchronized (this) { - ToneGenerator toneGen = getOrCreateToneGenerator(streamType); - if (toneGen != null) { - toneGen.startTone(ToneGenerator.TONE_PROP_BEEP); - sendMessageDelayed(obtainMessage(MSG_STOP_SOUNDS), BEEP_DURATION); - } - } - } - - protected void onStopSounds() { - - synchronized (this) { - int numStreamTypes = AudioSystem.getNumStreamTypes(); - for (int i = numStreamTypes - 1; i >= 0; i--) { - ToneGenerator toneGen = mToneGenerators[i]; - if (toneGen != null) { - toneGen.stopTone(); - } - } - } - } - - protected void onVibrate() { - - // Make sure we ended up in vibrate ringer mode - if (mAudioManager.getRingerMode() != AudioManager.RINGER_MODE_VIBRATE) { - return; - } - - mVibrator.vibrate(VIBRATE_DURATION, AudioManager.STREAM_SYSTEM); - } - - protected void onRemoteVolumeChanged(int streamType, int flags) { - // streamType is the real stream type being affected, but for the UI sliders, we - // refer to AudioService.STREAM_REMOTE_MUSIC. We still play the beeps on the real - // stream type. - if (LOGD) Log.d(TAG, "onRemoteVolumeChanged(stream:"+streamType+", flags: " + flags + ")"); - - if (((flags & AudioManager.FLAG_SHOW_UI) != 0) || mDialog.isShowing()) { - synchronized (this) { - if (mActiveStreamType != AudioService.STREAM_REMOTE_MUSIC) { - reorderSliders(AudioService.STREAM_REMOTE_MUSIC); - } - onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, flags); - } - } else { - if (LOGD) Log.d(TAG, "not calling onShowVolumeChanged(), no FLAG_SHOW_UI or no UI"); - } - - if ((flags & AudioManager.FLAG_PLAY_SOUND) != 0 && ! mRingIsSilent) { - removeMessages(MSG_PLAY_SOUND); - sendMessageDelayed(obtainMessage(MSG_PLAY_SOUND, streamType, flags), PLAY_SOUND_DELAY); - } - - if ((flags & AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE) != 0) { - removeMessages(MSG_PLAY_SOUND); - removeMessages(MSG_VIBRATE); - onStopSounds(); - } - - removeMessages(MSG_FREE_RESOURCES); - sendMessageDelayed(obtainMessage(MSG_FREE_RESOURCES), FREE_DELAY); - resetTimeout(); - } - - protected void onRemoteVolumeUpdateIfShown() { - if (LOGD) Log.d(TAG, "onRemoteVolumeUpdateIfShown()"); - if (mDialog.isShowing() - && (mActiveStreamType == AudioService.STREAM_REMOTE_MUSIC) - && (mStreamControls != null)) { - onShowVolumeChanged(AudioService.STREAM_REMOTE_MUSIC, 0); - } - } - - - /** - * Handler for MSG_SLIDER_VISIBILITY_CHANGED - * Hide or show a slider - * @param streamType can be a valid stream type value, or VolumePanel.STREAM_MASTER, - * or AudioService.STREAM_REMOTE_MUSIC - * @param visible - */ - synchronized protected void onSliderVisibilityChanged(int streamType, int visible) { - if (LOGD) Log.d(TAG, "onSliderVisibilityChanged(stream="+streamType+", visi="+visible+")"); - boolean isVisible = (visible == 1); - for (int i = STREAMS.length - 1 ; i >= 0 ; i--) { - StreamResources streamRes = STREAMS[i]; - if (streamRes.streamType == streamType) { - streamRes.show = isVisible; - if (!isVisible && (mActiveStreamType == streamType)) { - mActiveStreamType = -1; - } - break; - } - } - } - - protected void onDisplaySafeVolumeWarning(int flags) { - if ((flags & AudioManager.FLAG_SHOW_UI) != 0 || mDialog.isShowing()) { - synchronized (sConfirmSafeVolumeLock) { - if (sConfirmSafeVolumeDialog != null) { - return; - } - sConfirmSafeVolumeDialog = new AlertDialog.Builder(mContext) - .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(); - } - }) - .setNegativeButton(com.android.internal.R.string.no, null) - .setIconAttribute(android.R.attr.alertDialogIcon) - .create(); - final WarningDialogReceiver warning = new WarningDialogReceiver(mContext, - sConfirmSafeVolumeDialog, this); - - sConfirmSafeVolumeDialog.setOnDismissListener(warning); - sConfirmSafeVolumeDialog.getWindow().setType( - WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG); - sConfirmSafeVolumeDialog.show(); - } - updateStates(); - } - resetTimeout(); - } - - /** - * Lock on this VolumePanel instance as long as you use the returned ToneGenerator. - */ - private ToneGenerator getOrCreateToneGenerator(int streamType) { - if (streamType == STREAM_MASTER) { - // For devices that use the master volume setting only but still want to - // play a volume-changed tone, direct the master volume pseudostream to - // the system stream's tone generator. - if (mPlayMasterStreamTones) { - streamType = AudioManager.STREAM_SYSTEM; - } else { - return null; - } - } - synchronized (this) { - if (mToneGenerators[streamType] == null) { - try { - mToneGenerators[streamType] = new ToneGenerator(streamType, MAX_VOLUME); - } catch (RuntimeException e) { - if (LOGD) { - Log.d(TAG, "ToneGenerator constructor failed with " - + "RuntimeException: " + e); - } - } - } - return mToneGenerators[streamType]; - } - } - - - /** - * Switch between icons because Bluetooth music is same as music volume, but with - * different icons. - */ - private void setMusicIcon(int resId, int resMuteId) { - StreamControl sc = mStreamControls.get(AudioManager.STREAM_MUSIC); - if (sc != null) { - sc.iconRes = resId; - sc.iconMuteRes = resMuteId; - sc.icon.setImageResource(isMuted(sc.streamType) ? sc.iconMuteRes : sc.iconRes); - } - } - - protected void onFreeResources() { - synchronized (this) { - for (int i = mToneGenerators.length - 1; i >= 0; i--) { - if (mToneGenerators[i] != null) { - mToneGenerators[i].release(); - } - mToneGenerators[i] = null; - } - } - } - - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - - case MSG_VOLUME_CHANGED: { - onVolumeChanged(msg.arg1, msg.arg2); - break; - } - - case MSG_MUTE_CHANGED: { - onMuteChanged(msg.arg1, msg.arg2); - break; - } - - case MSG_FREE_RESOURCES: { - onFreeResources(); - break; - } - - case MSG_STOP_SOUNDS: { - onStopSounds(); - break; - } - - case MSG_PLAY_SOUND: { - onPlaySound(msg.arg1, msg.arg2); - break; - } - - case MSG_VIBRATE: { - onVibrate(); - break; - } - - case MSG_TIMEOUT: { - if (mDialog.isShowing()) { - mDialog.dismiss(); - mActiveStreamType = -1; - } - synchronized (sConfirmSafeVolumeLock) { - if (sConfirmSafeVolumeDialog != null) { - sConfirmSafeVolumeDialog.dismiss(); - } - } - break; - } - case MSG_RINGER_MODE_CHANGED: { - if (mDialog.isShowing()) { - updateStates(); - } - break; - } - - case MSG_REMOTE_VOLUME_CHANGED: { - onRemoteVolumeChanged(msg.arg1, msg.arg2); - break; - } - - case MSG_REMOTE_VOLUME_UPDATE_IF_SHOWN: - onRemoteVolumeUpdateIfShown(); - break; - - case MSG_SLIDER_VISIBILITY_CHANGED: - onSliderVisibilityChanged(msg.arg1, msg.arg2); - break; - - case MSG_DISPLAY_SAFE_VOLUME_WARNING: - onDisplaySafeVolumeWarning(msg.arg1); - break; - } - } - - private void resetTimeout() { - removeMessages(MSG_TIMEOUT); - sendMessageDelayed(obtainMessage(MSG_TIMEOUT), TIMEOUT_DELAY); - } - - private void forceTimeout() { - removeMessages(MSG_TIMEOUT); - sendMessage(obtainMessage(MSG_TIMEOUT)); - } - - 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(); - } - - @Override - public void onStartTrackingTouch(SeekBar seekBar) { - } - - @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)); - } - } - } - }; - - private final View.OnClickListener mClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - if (v == mMoreButton) { - expand(); - } - resetTimeout(); - } - }; -} diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 375f5e3..ecc4586 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -642,9 +642,7 @@ public abstract class Window { final WindowManager.LayoutParams attrs = getAttributes(); attrs.width = width; attrs.height = height; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -661,9 +659,7 @@ public abstract class Window { { final WindowManager.LayoutParams attrs = getAttributes(); attrs.gravity = gravity; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -675,9 +671,7 @@ public abstract class Window { public void setType(int type) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.type = type; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -700,9 +694,7 @@ public abstract class Window { attrs.format = mDefaultWindowFormat; mHaveWindowFormat = false; } - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -715,9 +707,7 @@ public abstract class Window { public void setWindowAnimations(int resId) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.windowAnimations = resId; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -735,9 +725,7 @@ public abstract class Window { } else { mHasSoftInputMode = false; } - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -793,14 +781,19 @@ public abstract class Window { attrs.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SET_NEEDS_MENU_KEY; } mForcedWindowFlags |= mask; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } private void setPrivateFlags(int flags, int mask) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.privateFlags = (attrs.privateFlags & ~mask) | (flags & mask); + dispatchWindowAttributesChanged(attrs); + } + + /** + * {@hide} + */ + protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) { if (mCallback != null) { mCallback.onWindowAttributesChanged(attrs); } @@ -818,9 +811,7 @@ public abstract class Window { final WindowManager.LayoutParams attrs = getAttributes(); attrs.dimAmount = amount; mHaveDimAmount = true; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } /** @@ -835,9 +826,7 @@ public abstract class Window { */ public void setAttributes(WindowManager.LayoutParams a) { mWindowAttributes.copyFrom(a); - if (mCallback != null) { - mCallback.onWindowAttributesChanged(mWindowAttributes); - } + dispatchWindowAttributesChanged(mWindowAttributes); } /** @@ -1269,9 +1258,7 @@ public abstract class Window { if (!mHaveWindowFormat) { final WindowManager.LayoutParams attrs = getAttributes(); attrs.format = format; - if (mCallback != null) { - mCallback.onWindowAttributesChanged(attrs); - } + dispatchWindowAttributesChanged(attrs); } } @@ -1527,4 +1514,44 @@ public abstract class Window { * until the called Activity's exiting transition completes. */ public boolean getAllowExitTransitionOverlap() { return true; } + + /** + * @return the color of the status bar. + */ + public abstract int getStatusBarColor(); + + /** + * Sets the color of the status bar to {@param color}. + * + * For this to take effect, + * the window must be drawing the system bar backgrounds with + * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and + * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_STATUS} must not be set. + * + * If {@param color} is not opaque, consider setting + * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and + * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN}. + */ + public abstract void setStatusBarColor(int color); + + /** + * @return the color of the navigation bar. + */ + public abstract int getNavigationBarColor(); + + /** + * Sets the color of the navigation bar to {@param color}. + * + * For this to take effect, + * the window must be drawing the system bar backgrounds with + * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and + * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set. + * + * If {@param color} is not opaque, consider setting + * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_STABLE} and + * {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION}. + */ + public abstract void setNavigationBarColor(int color); + + } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 032a82f..4eecc6a 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -218,7 +218,8 @@ public interface WindowManager extends ViewManager { @ViewDebug.IntToString(from = TYPE_NAVIGATION_BAR_PANEL, to = "TYPE_NAVIGATION_BAR_PANEL"), @ViewDebug.IntToString(from = TYPE_DISPLAY_OVERLAY, to = "TYPE_DISPLAY_OVERLAY"), @ViewDebug.IntToString(from = TYPE_MAGNIFICATION_OVERLAY, to = "TYPE_MAGNIFICATION_OVERLAY"), - @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION, to = "TYPE_PRIVATE_PRESENTATION") + @ViewDebug.IntToString(from = TYPE_PRIVATE_PRESENTATION, to = "TYPE_PRIVATE_PRESENTATION"), + @ViewDebug.IntToString(from = TYPE_VOICE_INTERACTION, to = "TYPE_VOICE_INTERACTION"), }) public int type; @@ -541,6 +542,12 @@ public interface WindowManager extends ViewManager { public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30; /** + * Window type: Windows in the voice interaction layer. + * @hide + */ + public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31; + + /** * End of types of system windows. */ public static final int LAST_SYSTEM_WINDOW = 2999; @@ -915,6 +922,14 @@ public interface WindowManager extends ViewManager { public static final int FLAG_NEEDS_MENU_KEY = 0x40000000; /** + * Flag indicating that this Window is responsible for drawing the background for the + * system bars. If set, the system bars are drawn with a transparent background and the + * corresponding areas in this window are filled with the colors specified in + * {@link Window#getStatusBarColor()} and {@link Window#getNavigationBarColor()}. + */ + public static final int FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS = 0x80000000; + + /** * Various behavioral options/flags. Default is none. * * @see #FLAG_ALLOW_LOCK_WHILE_SCREEN_ON @@ -941,6 +956,7 @@ public interface WindowManager extends ViewManager { * @see #FLAG_SPLIT_TOUCH * @see #FLAG_HARDWARE_ACCELERATED * @see #FLAG_LOCAL_FOCUS_MODE + * @see #FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS */ @ViewDebug.ExportedProperty(flagMapping = { @ViewDebug.FlagToString(mask = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, equals = FLAG_ALLOW_LOCK_WHILE_SCREEN_ON, @@ -998,7 +1014,9 @@ public interface WindowManager extends ViewManager { @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_STATUS, equals = FLAG_TRANSLUCENT_STATUS, name = "FLAG_TRANSLUCENT_STATUS"), @ViewDebug.FlagToString(mask = FLAG_TRANSLUCENT_NAVIGATION, equals = FLAG_TRANSLUCENT_NAVIGATION, - name = "FLAG_TRANSLUCENT_NAVIGATION") + name = "FLAG_TRANSLUCENT_NAVIGATION"), + @ViewDebug.FlagToString(mask = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, equals = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + name = "FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS") }) public int flags; diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index 96c0ed2..b4779f4 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -430,7 +430,7 @@ public final class WindowManagerGlobal { HardwareRenderer renderer = root.getView().mAttachInfo.mHardwareRenderer; if (renderer != null) { - renderer.dumpGfxInfo(pw); + renderer.dumpGfxInfo(pw, fd); } } @@ -447,11 +447,6 @@ public final class WindowManagerGlobal { String name = getWindowName(root); pw.printf(" %s\n %d views, %.2f kB of display lists", name, info[0], info[1] / 1024.0f); - HardwareRenderer renderer = - root.getView().mAttachInfo.mHardwareRenderer; - if (renderer != null) { - pw.printf(", %d frames rendered", renderer.getFrameCount()); - } pw.printf("\n\n"); viewsCount += info[0]; diff --git a/core/java/android/view/WindowManagerPolicy.java b/core/java/android/view/WindowManagerPolicy.java index 4fde1e4..2b4677c 100644 --- a/core/java/android/view/WindowManagerPolicy.java +++ b/core/java/android/view/WindowManagerPolicy.java @@ -274,6 +274,11 @@ public interface WindowManagerPolicy { public IApplicationToken getAppToken(); /** + * Return true if this window is participating in voice interaction. + */ + public boolean isVoiceInteraction(); + + /** * Return true if, at any point, the application token associated with * this window has actually displayed any windows. This is most useful * with the "starting up" window to determine if any windows were @@ -603,8 +608,15 @@ public interface WindowManagerPolicy { * Return whether the given window should forcibly hide everything * behind it. Typically returns true for the keyguard. */ - public boolean doesForceHide(WindowState win, WindowManager.LayoutParams attrs); - + public boolean doesForceHide(WindowManager.LayoutParams attrs); + + + /** + * Return whether the given window can become one that passes doesForceHide() test. + * Typically returns true for the StatusBar. + */ + public boolean isKeyguardHostWindow(WindowManager.LayoutParams attrs); + /** * Determine if a window that is behind one that is force hiding * (as determined by {@link #doesForceHide}) should actually be hidden. @@ -613,7 +625,7 @@ public interface WindowManagerPolicy { * will conflict with what you set. */ public boolean canBeForceHidden(WindowState win, WindowManager.LayoutParams attrs); - + /** * Called when the system would like to show a UI to indicate that an * application is starting. You can use this to add a @@ -1184,4 +1196,12 @@ public interface WindowManagerPolicy { * @return True if the window is a top level one. */ public boolean isTopLevelWindow(int windowType); + + /** + * Notifies the keyguard to start fading out. + * + * @param startTime the start time of the animation in uptime milliseconds + * @param fadeoutDuration the duration of the exit animation, in milliseconds + */ + public void startKeyguardExitAnimation(long startTime, long fadeoutDuration); } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index 9d10930..66cc3f6 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -17,15 +17,19 @@ package android.view.accessibility; import android.accessibilityservice.AccessibilityServiceInfo; +import android.annotation.Nullable; import android.graphics.Rect; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.text.InputType; +import android.text.TextUtils; +import android.util.ArraySet; import android.util.LongArray; import android.util.Pools.SynchronizedPool; import android.view.View; +import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -112,7 +116,7 @@ public class AccessibilityNodeInfo implements Parcelable { public static final int ACTION_SELECT = 0x00000004; /** - * Action that unselects the node. + * Action that deselects the node. */ public static final int ACTION_CLEAR_SELECTION = 0x00000008; @@ -307,6 +311,13 @@ public class AccessibilityNodeInfo implements Parcelable { */ public static final int ACTION_SET_TEXT = 0x00200000; + private static final int LAST_LEGACY_STANDARD_ACTION = ACTION_SET_TEXT; + + /** + * Mask to see if the value is larger than the largest ACTION_ constant + */ + private static final int ACTION_TYPE_MASK = 0xFF000000; + // Action arguments /** @@ -548,7 +559,7 @@ public class AccessibilityNodeInfo implements Parcelable { private String mViewIdResourceName; private LongArray mChildNodeIds; - private int mActions; + private ArrayList<AccessibilityAction> mActions; private int mMovementGranularities; @@ -875,6 +886,17 @@ public class AccessibilityNodeInfo implements Parcelable { /** * Gets the actions that can be performed on the node. + */ + public List<AccessibilityAction> getActionList() { + if (mActions == null) { + return Collections.emptyList(); + } + + return mActions; + } + + /** + * Gets the actions that can be performed on the node. * * @return The bit mask of with actions. * @@ -892,9 +914,61 @@ public class AccessibilityNodeInfo implements Parcelable { * @see AccessibilityNodeInfo#ACTION_PREVIOUS_HTML_ELEMENT * @see AccessibilityNodeInfo#ACTION_SCROLL_FORWARD * @see AccessibilityNodeInfo#ACTION_SCROLL_BACKWARD + * + * @deprecated Use {@link #getActionList()}. */ + @Deprecated public int getActions() { - return mActions; + int returnValue = 0; + + if (mActions == null) { + return returnValue; + } + + final int actionSize = mActions.size(); + for (int i = 0; i < actionSize; i++) { + int actionId = mActions.get(i).getId(); + if (actionId <= LAST_LEGACY_STANDARD_ACTION) { + returnValue |= actionId; + } + } + + return returnValue; + } + + /** + * Adds an action that can be performed on the node. + * <p> + * To add a standard action use the static constants on {@link AccessibilityAction}. + * To add a custom action create a new {@link AccessibilityAction} by passing in a + * resource id from your application as the action id and an optional label that + * describes the action. To override one of the standard actions use as the action + * id of a standard action id such as {@link #ACTION_CLICK} and an optional label that + * describes the action. + * </p> + * <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 addAction(AccessibilityAction action) { + enforceNotSealed(); + + if (action == null) { + return; + } + + if (mActions == null) { + mActions = new ArrayList<AccessibilityAction>(); + } + + mActions.remove(action); + mActions.add(action); } /** @@ -908,10 +982,20 @@ public class AccessibilityNodeInfo implements Parcelable { * @param action The action. * * @throws IllegalStateException If called from an AccessibilityService. + * @throws IllegalArgumentException If the argument is not one of the standard actions. + * + * @deprecated This has been deprecated for {@link #addAction(AccessibilityAction)} */ + @Deprecated public void addAction(int action) { enforceNotSealed(); - mActions |= action; + + if ((action & ACTION_TYPE_MASK) != 0) { + throw new IllegalArgumentException("Action is not a combination of the standard " + + "actions: " + action); + } + + addLegacyStandardActions(action); } /** @@ -923,13 +1007,40 @@ public class AccessibilityNodeInfo implements Parcelable { * This class is made immutable before being delivered to an AccessibilityService. * </p> * - * @param action The action. + * @param action The action to be removed. * * @throws IllegalStateException If called from an AccessibilityService. + * @deprecated Use {@link #removeAction(AccessibilityAction)} */ + @Deprecated public void removeAction(int action) { enforceNotSealed(); - mActions &= ~action; + + removeAction(getActionSingleton(action)); + } + + /** + * 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 to be removed. + * @return The action removed from the list of actions. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public boolean removeAction(AccessibilityAction action) { + enforceNotSealed(); + + if (mActions == null || action == null) { + return false; + } + + return mActions.remove(action); } /** @@ -2307,7 +2418,29 @@ public class AccessibilityNodeInfo implements Parcelable { parcel.writeInt(mBoundsInScreen.left); parcel.writeInt(mBoundsInScreen.right); - parcel.writeInt(mActions); + if (mActions != null && !mActions.isEmpty()) { + final int actionCount = mActions.size(); + parcel.writeInt(actionCount); + + int defaultLegacyStandardActions = 0; + for (int i = 0; i < actionCount; i++) { + AccessibilityAction action = mActions.get(i); + if (isDefaultLegacyStandardAction(action)) { + defaultLegacyStandardActions |= action.getId(); + } + } + parcel.writeInt(defaultLegacyStandardActions); + + for (int i = 0; i < actionCount; i++) { + AccessibilityAction action = mActions.get(i); + if (!isDefaultLegacyStandardAction(action)) { + parcel.writeInt(action.getId()); + parcel.writeCharSequence(action.getLabel()); + } + } + } else { + parcel.writeInt(0); + } parcel.writeInt(mMovementGranularities); @@ -2388,7 +2521,17 @@ public class AccessibilityNodeInfo implements Parcelable { mText = other.mText; mContentDescription = other.mContentDescription; mViewIdResourceName = other.mViewIdResourceName; - mActions= other.mActions; + + final ArrayList<AccessibilityAction> otherActions = other.mActions; + if (otherActions != null && otherActions.size() > 0) { + if (mActions == null) { + mActions = new ArrayList(otherActions); + } else { + mActions.clear(); + mActions.addAll(other.mActions); + } + } + mBooleanProperties = other.mBooleanProperties; mMovementGranularities = other.mMovementGranularities; @@ -2452,7 +2595,17 @@ public class AccessibilityNodeInfo implements Parcelable { mBoundsInScreen.left = parcel.readInt(); mBoundsInScreen.right = parcel.readInt(); - mActions = parcel.readInt(); + final int actionCount = parcel.readInt(); + if (actionCount > 0) { + final int legacyStandardActions = parcel.readInt(); + addLegacyStandardActions(legacyStandardActions); + final int nonLegacyActionCount = actionCount - Integer.bitCount(legacyStandardActions); + for (int i = 0; i < nonLegacyActionCount; i++) { + AccessibilityAction action = new AccessibilityAction( + parcel.readInt(), parcel.readCharSequence()); + addAction(action); + } + } mMovementGranularities = parcel.readInt(); @@ -2524,7 +2677,9 @@ public class AccessibilityNodeInfo implements Parcelable { mText = null; mContentDescription = null; mViewIdResourceName = null; - mActions = 0; + if (mActions != null) { + mActions.clear(); + } mTextSelectionStart = UNDEFINED_SELECTION_INDEX; mTextSelectionEnd = UNDEFINED_SELECTION_INDEX; mInputType = InputType.TYPE_NULL; @@ -2546,6 +2701,33 @@ public class AccessibilityNodeInfo implements Parcelable { } } + private static boolean isDefaultLegacyStandardAction(AccessibilityAction action) { + return (action.getId() <= LAST_LEGACY_STANDARD_ACTION + && TextUtils.isEmpty(action.getLabel())); + } + + private static AccessibilityAction getActionSingleton(int actionId) { + final int actions = AccessibilityAction.sStandardActions.size(); + for (int i = 0; i < actions; i++) { + AccessibilityAction currentAction = AccessibilityAction.sStandardActions.valueAt(i); + if (actionId == currentAction.getId()) { + return currentAction; + } + } + + return null; + } + + private void addLegacyStandardActions(int actionMask) { + int remainingIds = actionMask; + while (remainingIds > 0) { + final int id = 1 << Integer.numberOfTrailingZeros(remainingIds); + remainingIds &= ~id; + AccessibilityAction action = getActionSingleton(id); + addAction(action); + } + } + /** * Gets the human readable action symbolic name. * @@ -2709,20 +2891,425 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; longClickable: ").append(isLongClickable()); builder.append("; enabled: ").append(isEnabled()); builder.append("; password: ").append(isPassword()); - builder.append("; scrollable: " + isScrollable()); - - builder.append("; ["); - for (int actionBits = mActions; actionBits != 0;) { - final int action = 1 << Integer.numberOfTrailingZeros(actionBits); - actionBits &= ~action; - builder.append(getActionSymbolicName(action)); - if (actionBits != 0) { - builder.append(", "); + builder.append("; scrollable: ").append(isScrollable()); + builder.append("; actions: ").append(mActions); + + return builder.toString(); + } + + /** + * A class defining an action that can be performed on an {@link AccessibilityNodeInfo}. + * Each action has a unique id that is mandatory and optional data. + * <p> + * There are three categories of actions: + * <ul> + * <li><strong>Standard actions</strong> - These are actions that are reported and + * handled by the standard UI widgets in the platform. For each standard action + * there is a static constant defined in this class, e.g. {@link #ACTION_FOCUS}. + * </li> + * <li><strong>Custom actions action</strong> - These are actions that are reported + * and handled by custom widgets. i.e. ones that are not part of the UI toolkit. For + * example, an application may define a custom action for clearing the user history. + * </li> + * <li><strong>Overriden standard actions</strong> - These are actions that override + * standard actions to customize them. For example, an app may add a label to the + * standard click action to announce that this action clears browsing history. + * </ul> + * </p> + */ + public static final class AccessibilityAction { + + /** + * Action that gives input focus to the node. + */ + public static final AccessibilityAction ACTION_FOCUS = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_FOCUS, null); + + /** + * Action that clears input focus of the node. + */ + public static final AccessibilityAction ACTION_CLEAR_FOCUS = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_FOCUS, null); + + /** + * Action that selects the node. + */ + public static final AccessibilityAction ACTION_SELECT = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_SELECT, null); + + /** + * Action that deselects the node. + */ + public static final AccessibilityAction ACTION_CLEAR_SELECTION = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_SELECTION, null); + + /** + * Action that clicks on the node info. + */ + public static final AccessibilityAction ACTION_CLICK = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLICK, null); + + /** + * Action that long clicks on the node. + */ + public static final AccessibilityAction ACTION_LONG_CLICK = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_LONG_CLICK, null); + + /** + * Action that gives accessibility focus to the node. + */ + public static final AccessibilityAction ACTION_ACCESSIBILITY_FOCUS = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null); + + /** + * Action that clears accessibility focus of the node. + */ + public static final AccessibilityAction ACTION_CLEAR_ACCESSIBILITY_FOCUS = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null); + + /** + * Action that requests to go to the next entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + * <p> + * <strong>Arguments:</strong> + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}, + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br> + * <strong>Example:</strong> Move to the previous character and do not extend selection. + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityAction.ACTION_NEXT_AT_MOVEMENT_GRANULARITY.getId(), + * arguments); + * </code></pre></p> + * </p> + * + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see AccessibilityNodeInfo#setMovementGranularities(int) + * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * @see AccessibilityNodeInfo#getMovementGranularities() + * AccessibilityNodeInfo.getMovementGranularities() + * + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_CHARACTER + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_WORD + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_LINE + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PARAGRAPH + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PAGE + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE + */ + public static final AccessibilityAction ACTION_NEXT_AT_MOVEMENT_GRANULARITY = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, null); + + /** + * Action that requests to go to the previous entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + * <p> + * <strong>Arguments:</strong> + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}, + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}<br> + * <strong>Example:</strong> Move to the next character and do not extend selection. + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityAction.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY.getId(), + * arguments); + * </code></pre></p> + * </p> + * + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see AccessibilityNodeInfo#setMovementGranularities(int) + * AccessibilityNodeInfo.setMovementGranularities(int) + * @see AccessibilityNodeInfo#getMovementGranularities() + * AccessibilityNodeInfo.getMovementGranularities() + * + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_CHARACTER + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_WORD + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_LINE + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PARAGRAPH + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH + * @see AccessibilityNodeInfo#MOVEMENT_GRANULARITY_PAGE + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE + */ + public static final AccessibilityAction ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, null); + + /** + * Action to move to the next HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + * <p> + * <strong>Arguments:</strong> + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_HTML_ELEMENT_STRING + * AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br> + * <strong>Example:</strong> + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityAction.ACTION_NEXT_HTML_ELEMENT.getId(), arguments); + * </code></pre></p> + * </p> + */ + public static final AccessibilityAction ACTION_NEXT_HTML_ELEMENT = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null); + + /** + * Action to move to the previous HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + * <p> + * <strong>Arguments:</strong> + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_HTML_ELEMENT_STRING + * AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING}<br> + * <strong>Example:</strong> + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityAction.ACTION_PREVIOUS_HTML_ELEMENT.getId(), arguments); + * </code></pre></p> + * </p> + */ + public static final AccessibilityAction ACTION_PREVIOUS_HTML_ELEMENT = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, null); + + /** + * Action to scroll the node content forward. + */ + public static final AccessibilityAction ACTION_SCROLL_FORWARD = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null); + + /** + * Action to scroll the node content backward. + */ + public static final AccessibilityAction ACTION_SCROLL_BACKWARD = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null); + + /** + * Action to copy the current selection to the clipboard. + */ + public static final AccessibilityAction ACTION_COPY = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_COPY, null); + + /** + * Action to paste the current clipboard content. + */ + public static final AccessibilityAction ACTION_PASTE = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_PASTE, null); + + /** + * Action to cut the current selection and place it to the clipboard. + */ + public static final AccessibilityAction ACTION_CUT = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_CUT, null); + + /** + * Action to set the selection. Performing this action with no arguments + * clears the selection. + * <p> + * <strong>Arguments:</strong> + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_START_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT}, + * {@link AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_END_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT}<br> + * <strong>Example:</strong> + * <code><pre><p> + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2); + * info.performAction(AccessibilityAction.ACTION_SET_SELECTION.getId(), arguments); + * </code></pre></p> + * </p> + * + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_START_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT + * @see AccessibilityNodeInfo#ACTION_ARGUMENT_SELECTION_END_INT + * AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT + */ + public static final AccessibilityAction ACTION_SET_SELECTION = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_SET_SELECTION, null); + + /** + * Action to expand an expandable node. + */ + public static final AccessibilityAction ACTION_EXPAND = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_EXPAND, null); + + /** + * Action to collapse an expandable node. + */ + public static final AccessibilityAction ACTION_COLLAPSE = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_COLLAPSE, null); + + /** + * Action to dismiss a dismissable node. + */ + public static final AccessibilityAction ACTION_DISMISS = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_DISMISS, null); + + /** + * 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 AccessibilityNodeInfo#ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE + * AccessibilityNodeInfo.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(AccessibilityAction.ACTION_SET_TEXT.getId(), arguments); + * </code></pre></p> + */ + public static final AccessibilityAction ACTION_SET_TEXT = + new AccessibilityAction( + AccessibilityNodeInfo.ACTION_SET_TEXT, null); + + private static final ArraySet<AccessibilityAction> sStandardActions = new ArraySet<AccessibilityAction>(); + static { + sStandardActions.add(ACTION_FOCUS); + sStandardActions.add(ACTION_CLEAR_FOCUS); + sStandardActions.add(ACTION_SELECT); + sStandardActions.add(ACTION_CLEAR_SELECTION); + sStandardActions.add(ACTION_CLICK); + sStandardActions.add(ACTION_LONG_CLICK); + sStandardActions.add(ACTION_ACCESSIBILITY_FOCUS); + sStandardActions.add(ACTION_CLEAR_ACCESSIBILITY_FOCUS); + sStandardActions.add(ACTION_NEXT_AT_MOVEMENT_GRANULARITY); + sStandardActions.add(ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); + sStandardActions.add(ACTION_NEXT_HTML_ELEMENT); + sStandardActions.add(ACTION_PREVIOUS_HTML_ELEMENT); + sStandardActions.add(ACTION_SCROLL_FORWARD); + sStandardActions.add(ACTION_SCROLL_BACKWARD); + sStandardActions.add(ACTION_COPY); + sStandardActions.add(ACTION_PASTE); + sStandardActions.add(ACTION_CUT); + sStandardActions.add(ACTION_SET_SELECTION); + sStandardActions.add(ACTION_EXPAND); + sStandardActions.add(ACTION_COLLAPSE); + sStandardActions.add(ACTION_DISMISS); + sStandardActions.add(ACTION_SET_TEXT); + } + + private final int mActionId; + private final CharSequence mLabel; + + /** + * Creates a new AccessibilityAction. For adding a standard action without a specific label, + * use the static constants. + * + * You can also override the description for one the standard actions. Below is an example + * how to override the standard click action by adding a custom label: + * <pre> + * AccessibilityAction action = new AccessibilityAction( + * AccessibilityAction.ACTION_ACTION_CLICK, getLocalizedLabel()); + * node.addAction(action); + * </pre> + * + * @param actionId The id for this action. This should either be one of the + * standard actions or a specific action for your app. In that case it is + * required to use a resource identifier. + * @param label The label for the new AccessibilityAction. + */ + public AccessibilityAction(int actionId, @Nullable CharSequence label) { + if ((actionId & ACTION_TYPE_MASK) == 0 && Integer.bitCount(actionId) != 1) { + throw new IllegalArgumentException("Invalid standard action id"); } + + mActionId = actionId; + mLabel = label; } - builder.append("]"); - return builder.toString(); + /** + * Gets the id for this action. + * + * @return The action id. + */ + public int getId() { + return mActionId; + } + + /** + * Gets the label for this action. Its purpose is to describe the + * action to user. + * + * @return The label. + */ + public CharSequence getLabel() { + return mLabel; + } + + @Override + public int hashCode() { + return mActionId; + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + if (other == this) { + return true; + } + + if (getClass() != other.getClass()) { + return false; + } + + return mActionId == ((AccessibilityAction)other).mActionId; + } + + @Override + public String toString() { + return "AccessibilityAction: " + getActionSymbolicName(mActionId) + " - " + mLabel; + } } /** diff --git a/core/java/android/view/animation/AccelerateDecelerateInterpolator.java b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java index 158c56e..ed6949a 100644 --- a/core/java/android/view/animation/AccelerateDecelerateInterpolator.java +++ b/core/java/android/view/animation/AccelerateDecelerateInterpolator.java @@ -19,12 +19,17 @@ package android.view.animation; import android.content.Context; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the rate of change starts and ends slowly but * accelerates through the middle. * */ -public class AccelerateDecelerateInterpolator implements Interpolator { +@HasNativeInterpolator +public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory { public AccelerateDecelerateInterpolator() { } @@ -35,4 +40,10 @@ public class AccelerateDecelerateInterpolator implements Interpolator { public float getInterpolation(float input) { return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f; } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator(); + } } diff --git a/core/java/android/view/animation/AccelerateInterpolator.java b/core/java/android/view/animation/AccelerateInterpolator.java index dcab743..c08f348 100644 --- a/core/java/android/view/animation/AccelerateInterpolator.java +++ b/core/java/android/view/animation/AccelerateInterpolator.java @@ -20,12 +20,17 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the rate of change starts out slowly and * and then accelerates. * */ -public class AccelerateInterpolator implements Interpolator { +@HasNativeInterpolator +public class AccelerateInterpolator implements Interpolator, NativeInterpolatorFactory { private final float mFactor; private final double mDoubleFactor; @@ -64,4 +69,10 @@ public class AccelerateInterpolator implements Interpolator { return (float)Math.pow(input, mDoubleFactor); } } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createAccelerateInterpolator(mFactor); + } } diff --git a/core/java/android/view/animation/AnticipateInterpolator.java b/core/java/android/view/animation/AnticipateInterpolator.java index a6f110e..83a8007 100644 --- a/core/java/android/view/animation/AnticipateInterpolator.java +++ b/core/java/android/view/animation/AnticipateInterpolator.java @@ -20,10 +20,15 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the change starts backward then flings forward. */ -public class AnticipateInterpolator implements Interpolator { +@HasNativeInterpolator +public class AnticipateInterpolator implements Interpolator, NativeInterpolatorFactory { private final float mTension; public AnticipateInterpolator() { @@ -53,4 +58,10 @@ public class AnticipateInterpolator implements Interpolator { // a(t) = t * t * ((tension + 1) * t - tension) return t * t * ((mTension + 1) * t - mTension); } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createAnticipateInterpolator(mTension); + } } diff --git a/core/java/android/view/animation/AnticipateOvershootInterpolator.java b/core/java/android/view/animation/AnticipateOvershootInterpolator.java index 3dc9722..1a8adfd 100644 --- a/core/java/android/view/animation/AnticipateOvershootInterpolator.java +++ b/core/java/android/view/animation/AnticipateOvershootInterpolator.java @@ -19,6 +19,11 @@ package android.view.animation; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; + +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + import static com.android.internal.R.styleable.AnticipateOvershootInterpolator_extraTension; import static com.android.internal.R.styleable.AnticipateOvershootInterpolator_tension; import static com.android.internal.R.styleable.AnticipateOvershootInterpolator; @@ -27,7 +32,8 @@ import static com.android.internal.R.styleable.AnticipateOvershootInterpolator; * An interpolator where the change starts backward then flings forward and overshoots * the target value and finally goes back to the final value. */ -public class AnticipateOvershootInterpolator implements Interpolator { +@HasNativeInterpolator +public class AnticipateOvershootInterpolator implements Interpolator, NativeInterpolatorFactory { private final float mTension; public AnticipateOvershootInterpolator() { @@ -80,4 +86,10 @@ public class AnticipateOvershootInterpolator implements Interpolator { if (t < 0.5f) return 0.5f * a(t * 2.0f, mTension); else return 0.5f * (o(t * 2.0f - 2.0f, mTension) + 2.0f); } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createAnticipateOvershootInterpolator(mTension); + } } diff --git a/core/java/android/view/animation/BounceInterpolator.java b/core/java/android/view/animation/BounceInterpolator.java index ecf99a7..9d8ca90 100644 --- a/core/java/android/view/animation/BounceInterpolator.java +++ b/core/java/android/view/animation/BounceInterpolator.java @@ -19,10 +19,15 @@ package android.view.animation; import android.content.Context; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the change bounces at the end. */ -public class BounceInterpolator implements Interpolator { +@HasNativeInterpolator +public class BounceInterpolator implements Interpolator, NativeInterpolatorFactory { public BounceInterpolator() { } @@ -47,4 +52,10 @@ public class BounceInterpolator implements Interpolator { else if (t < 0.9644f) return bounce(t - 0.8526f) + 0.9f; else return bounce(t - 1.0435f) + 0.95f; } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createBounceInterpolator(); + } }
\ No newline at end of file diff --git a/core/java/android/view/animation/CycleInterpolator.java b/core/java/android/view/animation/CycleInterpolator.java index d355c23..d1ebf05 100644 --- a/core/java/android/view/animation/CycleInterpolator.java +++ b/core/java/android/view/animation/CycleInterpolator.java @@ -20,12 +20,17 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * Repeats the animation for a specified number of cycles. The * rate of change follows a sinusoidal pattern. * */ -public class CycleInterpolator implements Interpolator { +@HasNativeInterpolator +public class CycleInterpolator implements Interpolator, NativeInterpolatorFactory { public CycleInterpolator(float cycles) { mCycles = cycles; } @@ -44,4 +49,10 @@ public class CycleInterpolator implements Interpolator { } private float mCycles; + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createCycleInterpolator(mCycles); + } } diff --git a/core/java/android/view/animation/DecelerateInterpolator.java b/core/java/android/view/animation/DecelerateInterpolator.java index 20e079b..0789a0e 100644 --- a/core/java/android/view/animation/DecelerateInterpolator.java +++ b/core/java/android/view/animation/DecelerateInterpolator.java @@ -20,12 +20,17 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the rate of change starts out quickly and * and then decelerates. * */ -public class DecelerateInterpolator implements Interpolator { +@HasNativeInterpolator +public class DecelerateInterpolator implements Interpolator, NativeInterpolatorFactory { public DecelerateInterpolator() { } @@ -60,4 +65,10 @@ public class DecelerateInterpolator implements Interpolator { } private float mFactor = 1.0f; + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createDecelerateInterpolator(mFactor); + } } diff --git a/core/java/android/view/animation/LinearInterpolator.java b/core/java/android/view/animation/LinearInterpolator.java index 96a039f..552c611 100644 --- a/core/java/android/view/animation/LinearInterpolator.java +++ b/core/java/android/view/animation/LinearInterpolator.java @@ -19,11 +19,16 @@ package android.view.animation; import android.content.Context; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the rate of change is constant * */ -public class LinearInterpolator implements Interpolator { +@HasNativeInterpolator +public class LinearInterpolator implements Interpolator, NativeInterpolatorFactory { public LinearInterpolator() { } @@ -34,4 +39,10 @@ public class LinearInterpolator implements Interpolator { public float getInterpolation(float input) { return input; } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createLinearInterpolator(); + } } diff --git a/core/java/android/view/animation/OvershootInterpolator.java b/core/java/android/view/animation/OvershootInterpolator.java index 494f8ab..a2466f1 100644 --- a/core/java/android/view/animation/OvershootInterpolator.java +++ b/core/java/android/view/animation/OvershootInterpolator.java @@ -20,11 +20,16 @@ import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; +import com.android.internal.view.animation.HasNativeInterpolator; +import com.android.internal.view.animation.NativeInterpolatorFactory; +import com.android.internal.view.animation.NativeInterpolatorFactoryHelper; + /** * An interpolator where the change flings forward and overshoots the last value * then comes back. */ -public class OvershootInterpolator implements Interpolator { +@HasNativeInterpolator +public class OvershootInterpolator implements Interpolator, NativeInterpolatorFactory { private final float mTension; public OvershootInterpolator() { @@ -56,4 +61,10 @@ public class OvershootInterpolator implements Interpolator { t -= 1.0f; return t * t * ((mTension + 1) * t + mTension) + 1.0f; } + + /** @hide */ + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createOvershootInterpolator(mTension); + } } diff --git a/core/java/android/view/inputmethod/CursorAnchorInfo.java b/core/java/android/view/inputmethod/CursorAnchorInfo.java index 92455df..fad6747 100644 --- a/core/java/android/view/inputmethod/CursorAnchorInfo.java +++ b/core/java/android/view/inputmethod/CursorAnchorInfo.java @@ -35,8 +35,12 @@ import java.util.Objects; public final class CursorAnchorInfo implements Parcelable { private final int mSelectionStart; private final int mSelectionEnd; - private final int mCandidatesStart; - private final int mCandidatesEnd; + + private final int mComposingTextStart; + /** + * The text, tracked as a composing region. + */ + private final String mComposingText; /** * Horizontal position of the insertion marker, in the local coordinates that will be @@ -83,8 +87,8 @@ public final class CursorAnchorInfo implements Parcelable { public CursorAnchorInfo(final Parcel source) { mSelectionStart = source.readInt(); mSelectionEnd = source.readInt(); - mCandidatesStart = source.readInt(); - mCandidatesEnd = source.readInt(); + mComposingTextStart = source.readInt(); + mComposingText = source.readString(); mInsertionMarkerHorizontal = source.readFloat(); mInsertionMarkerTop = source.readFloat(); mInsertionMarkerBaseline = source.readFloat(); @@ -104,8 +108,8 @@ public final class CursorAnchorInfo implements Parcelable { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(mSelectionStart); dest.writeInt(mSelectionEnd); - dest.writeInt(mCandidatesStart); - dest.writeInt(mCandidatesEnd); + dest.writeInt(mComposingTextStart); + dest.writeString(mComposingText); dest.writeFloat(mInsertionMarkerHorizontal); dest.writeFloat(mInsertionMarkerTop); dest.writeFloat(mInsertionMarkerBaseline); @@ -119,14 +123,17 @@ public final class CursorAnchorInfo implements Parcelable { @Override public int hashCode(){ // TODO: Improve the hash function. - final float floatHash = mSelectionStart + mSelectionEnd + mCandidatesStart + mCandidatesEnd - + mInsertionMarkerHorizontal + mInsertionMarkerTop + mInsertionMarkerBaseline - + mInsertionMarkerBottom; + final float floatHash = mInsertionMarkerHorizontal + mInsertionMarkerTop + + mInsertionMarkerBaseline + mInsertionMarkerBottom; int hash = floatHash > 0 ? (int) floatHash : (int)(-floatHash); - if (mCharacterRects != null) { - hash += mCharacterRects.hashCode(); - } - hash += mMatrix.hashCode(); + hash *= 31; + hash += mSelectionStart + mSelectionEnd + mComposingTextStart; + hash *= 31; + hash += Objects.hashCode(mComposingText); + hash *= 31; + hash += Objects.hashCode(mCharacterRects); + hash *= 31; + hash += Objects.hashCode(mMatrix); return hash; } @@ -147,8 +154,10 @@ public final class CursorAnchorInfo implements Parcelable { } if (mSelectionStart != that.mSelectionStart || mSelectionEnd != that.mSelectionEnd - || mCandidatesStart != that.mCandidatesStart - || mCandidatesEnd != that.mCandidatesEnd) { + || mComposingTextStart != that.mComposingTextStart) { + return false; + } + if (!Objects.equals(mComposingTextStart, that.mComposingTextStart)) { return false; } if (!Objects.equals(mCharacterRects, that.mCharacterRects)) { @@ -163,13 +172,14 @@ public final class CursorAnchorInfo implements Parcelable { @Override public String toString() { return "SelectionInfo{mSelection=" + mSelectionStart + "," + mSelectionEnd - + " mCandiadtes=" + mCandidatesStart + "," + mCandidatesEnd + + " mComposingTextStart=" + mComposingTextStart + + " mComposingText=" + Objects.toString(mComposingText) + " mInsertionMarkerHorizontal=" + mInsertionMarkerHorizontal + " mInsertionMarkerTop=" + mInsertionMarkerTop + " mInsertionMarkerBaseline=" + mInsertionMarkerBaseline + " mInsertionMarkerBottom=" + mInsertionMarkerBottom - + " mCharacterRects=" + (mCharacterRects != null ? mCharacterRects : "null") - + " mMatrix=" + mMatrix + + " mCharacterRects=" + Objects.toString(mCharacterRects) + + " mMatrix=" + Objects.toString(mMatrix) + "}"; } @@ -190,16 +200,23 @@ public final class CursorAnchorInfo implements Parcelable { private int mSelectionEnd = -1; /** - * Sets the text range of the composition string. Calling this can be skipped if there is - * no composition. + * Sets the text range of the composing text. Calling this can be skipped if there is + * no composing text. + * @param index index where the composing text starts. + * @param composingText the entire composing text. */ - public CursorAnchorInfoBuilder setCandidateRange(final int start, final int end) { - mCandidateStart = start; - mCandidateEnd = end; + public CursorAnchorInfoBuilder setComposingText(final int index, + final CharSequence composingText) { + mComposingTextStart = index; + if (composingText == null) { + mComposingText = null; + } else { + mComposingText = composingText.toString(); + } return this; } - private int mCandidateStart = -1; - private int mCandidateEnd = -1; + private int mComposingTextStart = -1; + private String mComposingText = null; /** * Sets the location of the text insertion point (zero width cursor) as a rectangle in @@ -273,14 +290,10 @@ public final class CursorAnchorInfo implements Parcelable { * is interpreted as an identity matrix. */ public CursorAnchorInfoBuilder setMatrix(final Matrix matrix) { - if (matrix != null) { - mMatrix = matrix; - } else { - mMatrix = Matrix.IDENTITY_MATRIX; - } + mMatrix.set(matrix != null ? matrix : Matrix.IDENTITY_MATRIX); return this; } - private Matrix mMatrix = Matrix.IDENTITY_MATRIX; + private final Matrix mMatrix = new Matrix(Matrix.IDENTITY_MATRIX); /** * @return {@link CursorAnchorInfo} using parameters in this @@ -297,13 +310,13 @@ public final class CursorAnchorInfo implements Parcelable { public void reset() { mSelectionStart = -1; mSelectionEnd = -1; - mCandidateStart = -1; - mCandidateEnd = -1; + mComposingTextStart = -1; + mComposingText = null; mInsertionMarkerHorizontal = Float.NaN; mInsertionMarkerTop = Float.NaN; mInsertionMarkerBaseline = Float.NaN; mInsertionMarkerBottom = Float.NaN; - mMatrix = Matrix.IDENTITY_MATRIX; + mMatrix.set(Matrix.IDENTITY_MATRIX); if (mCharacterRectBuilder != null) { mCharacterRectBuilder.reset(); } @@ -313,15 +326,15 @@ public final class CursorAnchorInfo implements Parcelable { private CursorAnchorInfo(final CursorAnchorInfoBuilder builder) { mSelectionStart = builder.mSelectionStart; mSelectionEnd = builder.mSelectionEnd; - mCandidatesStart = builder.mCandidateStart; - mCandidatesEnd = builder.mCandidateEnd; + mComposingTextStart = builder.mComposingTextStart; + mComposingText = builder.mComposingText; mInsertionMarkerHorizontal = builder.mInsertionMarkerHorizontal; mInsertionMarkerTop = builder.mInsertionMarkerTop; mInsertionMarkerBaseline = builder.mInsertionMarkerBaseline; mInsertionMarkerBottom = builder.mInsertionMarkerBottom; mCharacterRects = builder.mCharacterRectBuilder != null ? builder.mCharacterRectBuilder.build() : null; - mMatrix = builder.mMatrix; + mMatrix = new Matrix(builder.mMatrix); } /** @@ -341,19 +354,19 @@ public final class CursorAnchorInfo implements Parcelable { } /** - * Returns the index where the composition starts. - * @return -1 if there is no composition. + * Returns the index where the composing text starts. + * @return -1 if there is no composing text. */ - public int getCandidatesStart() { - return mCandidatesStart; + public int getComposingTextStart() { + return mComposingTextStart; } /** - * Returns the index where the composition ends. - * @return -1 if there is no composition. + * Returns the entire composing text. + * @return null if there is no composition. */ - public int getCandidatesEnd() { - return mCandidatesEnd; + public String getComposingText() { + return mComposingText; } /** diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index e1c6f52..f874eb7 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -49,7 +49,6 @@ import android.view.InputEventSender; import android.view.KeyEvent; import android.view.View; import android.view.ViewRootImpl; -import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -57,6 +56,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -318,11 +318,16 @@ public final class InputMethodManager { int mCursorSelEnd; int mCursorCandStart; int mCursorCandEnd; + + /** + * The instance that has previously been sent to the input method. + */ + private CursorAnchorInfo mCursorAnchorInfo = null; + /** * The buffer to retrieve the view location in screen coordinates in {@link #updateCursor}. */ private final int[] mViewTopLeft = new int[2]; - private final CursorAnchorInfoBuilder mCursorAnchorInfoBuilder = new CursorAnchorInfoBuilder(); // ----------------------------------------------------------- @@ -492,6 +497,9 @@ public final class InputMethodManager { case SET_CURSOR_ANCHOR_MONITOR_MODE: { synchronized (mH) { mCursorAnchorMonitorMode = msg.arg1; + // Clear the cache. + mCursorRect.setEmpty(); + mCursorAnchorInfo = null; } return; } @@ -1176,6 +1184,7 @@ public final class InputMethodManager { mCursorCandStart = -1; mCursorCandEnd = -1; mCursorRect.setEmpty(); + mCursorAnchorInfo = null; servedContext = new ControlledInputConnectionWrapper(vh.getLooper(), ic, this); } else { servedContext = null; @@ -1538,10 +1547,9 @@ public final class InputMethodManager { || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } + if (DEBUG) Log.d(TAG, "updateCursor"); mTmpCursorRect.set(left, top, right, bottom); - if (!mCursorRect.equals(mTmpCursorRect)) { - if (DEBUG) Log.d(TAG, "updateCursor"); - + if (!Objects.equals(mCursorRect, mTmpCursorRect)) { try { if (DEBUG) Log.v(TAG, "CURSOR CHANGE: " + mCurMethod); mCursorRect.set(mTmpCursorRect); @@ -1572,10 +1580,14 @@ public final class InputMethodManager { || mCurrentTextBoxAttribute == null || mCurMethod == null) { return; } - if (DEBUG) Log.d(TAG, "updateCursorAnchorInfo"); - + if (Objects.equals(mCursorAnchorInfo, cursorAnchorInfo)) { + Log.w(TAG, "Ignoring redundant updateCursorAnchorInfo: info=" + cursorAnchorInfo); + return; + } + if (DEBUG) Log.v(TAG, "updateCursorAnchorInfo: " + cursorAnchorInfo); try { mCurMethod.updateCursorAnchorInfo(cursorAnchorInfo); + mCursorAnchorInfo = cursorAnchorInfo; } catch (RemoteException e) { Log.w(TAG, "IME died: " + mCurId, e); } diff --git a/core/java/android/view/textservice/SpellCheckerSession.java b/core/java/android/view/textservice/SpellCheckerSession.java index 628da3c..84f395a 100644 --- a/core/java/android/view/textservice/SpellCheckerSession.java +++ b/core/java/android/view/textservice/SpellCheckerSession.java @@ -427,8 +427,12 @@ public class SpellCheckerSession { @Override public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) { - mHandler.sendMessage( - Message.obtain(mHandler, MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); + synchronized (this) { + if (mHandler != null) { + mHandler.sendMessage(Message.obtain(mHandler, + MSG_ON_GET_SUGGESTION_MULTIPLE_FOR_SENTENCE, results)); + } + } } } diff --git a/core/java/android/webkit/BrowserDownloadListener.java b/core/java/android/webkit/BrowserDownloadListener.java deleted file mode 100644 index 724cc62..0000000 --- a/core/java/android/webkit/BrowserDownloadListener.java +++ /dev/null @@ -1,57 +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.webkit; - -/** - * An abstract download listener that allows passing extra information as - * part of onDownloadStart callback. - * @hide - */ -public abstract class BrowserDownloadListener implements DownloadListener { - - /** - * Notify the host application that a file should be downloaded - * @param url The full url to the content that should be downloaded - * @param userAgent the user agent to be used for the download. - * @param contentDisposition Content-disposition http header, if - * present. - * @param mimetype The mimetype of the content reported by the server - * @param referer The referer associated with this url - * @param contentLength The file size reported by the server - */ - public abstract void onDownloadStart(String url, String userAgent, - String contentDisposition, String mimetype, String referer, - long contentLength); - - - /** - * Notify the host application that a file should be downloaded - * @param url The full url to the content that should be downloaded - * @param userAgent the user agent to be used for the download. - * @param contentDisposition Content-disposition http header, if - * present. - * @param mimetype The mimetype of the content reported by the server - * @param contentLength The file size reported by the server - */ - @Override - public void onDownloadStart(String url, String userAgent, - String contentDisposition, String mimetype, long contentLength) { - - onDownloadStart(url, userAgent, contentDisposition, mimetype, null, - contentLength); - } -} diff --git a/core/java/android/webkit/EventLogTags.logtags b/core/java/android/webkit/EventLogTags.logtags index b0b5493..a90aebd 100644 --- a/core/java/android/webkit/EventLogTags.logtags +++ b/core/java/android/webkit/EventLogTags.logtags @@ -8,3 +8,4 @@ option java_package android.webkit; # 70103- used by the browser app itself 70150 browser_snap_center +70151 exp_det_attempt_to_call_object_getclass (app_signature|3) diff --git a/core/java/android/webkit/PermissionRequest.java b/core/java/android/webkit/PermissionRequest.java index 3e33498..fa760b7 100644 --- a/core/java/android/webkit/PermissionRequest.java +++ b/core/java/android/webkit/PermissionRequest.java @@ -28,6 +28,7 @@ import android.net.Uri; public interface PermissionRequest { /** * Resource belongs to geolocation service. + * @hide - see b/14668406 */ public final static long RESOURCE_GEOLOCATION = 1 << 0; /** diff --git a/core/java/android/webkit/WebBackForwardListClient.java b/core/java/android/webkit/WebBackForwardListClient.java deleted file mode 100644 index 7fe9281..0000000 --- a/core/java/android/webkit/WebBackForwardListClient.java +++ /dev/null @@ -1,40 +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.webkit; - -/** - * Interface to receive notifications when items are added to the - * {@link WebBackForwardList}. - * {@hide} - */ -public abstract class WebBackForwardListClient { - - /** - * Notify the client that <var>item</var> has been added to the - * WebBackForwardList. - * @param item The newly created WebHistoryItem - */ - public void onNewHistoryItem(WebHistoryItem item) { } - - /** - * Notify the client that the <var>item</var> at <var>index</var> is now - * the current history item. - * @param item A WebHistoryItem - * @param index The new history index - */ - public void onIndexChanged(WebHistoryItem item, int index) { } -} diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index 7c32c5b..d14c19b 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1460,4 +1460,36 @@ public abstract class WebSettings { * {@link #MIXED_CONTENT_NEVER_ALLOW} or {@link #MIXED_CONTENT_COMPATIBILITY_MODE}. */ public abstract int getMixedContentMode(); + + /** + * Sets whether to use a video overlay for embedded encrypted video. + * In API levels prior to {@link android.os.Build.VERSION_CODES#L}, encrypted video can + * only be rendered directly on a secure video surface, so it had been a hard problem to play + * encrypted video in HTML. When this flag is on, WebView can play encrypted video (MSE/EME) + * by using a video overlay (aka hole-punching) for videos embedded using HTML <video> + * tag.<br> + * Caution: This setting is intended for use only in a narrow set of circumstances and apps + * should only enable it if they require playback of encrypted video content. It will impose + * the following limitations on the WebView: + * <ul> + * <li> Only one video overlay can be played at a time. + * <li> Changes made to position or dimensions of a video element may be propagated to the + * corresponding video overlay with a noticeable delay. + * <li> The video overlay is not visible to web APIs and as such may not interact with + * script or styling. For example, CSS styles applied to the <video> tag may be ignored. + * </ul> + * This is not an exhaustive set of constraints and it may vary with new versions of the + * WebView. + * @hide + */ + public abstract void setVideoOverlayForEmbeddedEncryptedVideoEnabled(boolean flag); + + /** + * Gets whether a video overlay will be used for embedded encrypted video. + * + * @return true if WebView uses a video overlay for embedded encrypted video. + * @see #setVideoOverlayForEmbeddedEncryptedVideoEnabled + * @hide + */ + public abstract boolean getVideoOverlayForEmbeddedEncryptedVideoEnabled(); } diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 25bcd44..aaf0a75 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -30,6 +30,9 @@ public final class WebViewFactory { private static final String CHROMIUM_WEBVIEW_FACTORY = "com.android.webview.chromium.WebViewChromiumFactoryProvider"; + private static final String NULL_WEBVIEW_FACTORY = + "com.android.webview.nullwebview.NullWebViewFactoryProvider"; + private static final String LOGTAG = "WebViewFactory"; private static final boolean DEBUG = false; @@ -50,28 +53,6 @@ public final class WebViewFactory { private static WebViewFactoryProvider sProviderInstance; private static final Object sProviderLock = new Object(); - public static boolean isExperimentalWebViewAvailable() { - // TODO: Remove callers of this method then remove it. - return false; // Hide the toggle in Developer Settings. - } - - /** @hide */ - public static void setUseExperimentalWebView(boolean enable) { - // TODO: Remove callers of this method then remove it. - } - - /** @hide */ - public static boolean useExperimentalWebView() { - // TODO: Remove callers of this method then remove it. - return true; - } - - /** @hide */ - public static boolean isUseExperimentalWebViewSet() { - // TODO: Remove callers of this method then remove it. - return false; // User has not modifed Developer Settings - } - static WebViewFactoryProvider getProvider() { synchronized (sProviderLock) { // For now the main purpose of this function (and the factory abstraction) is to keep @@ -110,6 +91,11 @@ public final class WebViewFactory { } private static Class<WebViewFactoryProvider> getFactoryClass() throws ClassNotFoundException { - return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY); + try { + return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY); + } catch (ClassNotFoundException e) { + Log.e(LOGTAG, "Chromium WebView does not exist"); + return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY); + } } } diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index f4cd5fc..9a46052 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -743,7 +743,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te * * @param view The view whose scroll state is being reported * - * @param scrollState The current scroll state. One of + * @param scrollState The current scroll state. One of * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}. */ public void onScrollStateChanged(AbsListView view, int scrollState); @@ -2495,29 +2495,31 @@ 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. + * Positions the selector in a way that mimics keyboard focus. */ void positionSelectorLikeFocus(int position, View sel) { + // If we're changing position, update the visibility since the selector + // is technically being detached from the previous selection. + final Drawable selector = mSelector; + final boolean manageState = selector != null && mSelectorPosition != position + && position != INVALID_POSITION; + if (manageState) { + selector.setVisible(false, false); + } + positionSelector(position, sel); - final Drawable selector = mSelector; - if (selector != null && selector.supportsHotspots() && position != INVALID_POSITION) { + if (manageState) { final Rect bounds = mSelectorRect; final float x = bounds.exactCenterX(); final float y = bounds.exactCenterY(); - selector.setHotspot(R.attr.state_focused, x, y); + selector.setVisible(getVisibility() == VISIBLE, false); + selector.setHotspot(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; } @@ -2526,8 +2528,18 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (sel instanceof SelectionBoundsAdjuster) { ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect); } - positionSelector(selectorRect.left, selectorRect.top, selectorRect.right, - selectorRect.bottom); + + // Adjust for selection padding. + selectorRect.left -= mSelectionLeftPadding; + selectorRect.top -= mSelectionTopPadding; + selectorRect.right += mSelectionRightPadding; + selectorRect.bottom += mSelectionBottomPadding; + + // Update the selector drawable. + final Drawable selector = mSelector; + if (selector != null) { + selector.setBounds(selectorRect); + } final boolean isChildViewEnabled = mIsChildViewEnabled; if (sel.isEnabled() != isChildViewEnabled) { @@ -2538,11 +2550,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - private void positionSelector(int l, int t, int r, int b) { - mSelectorRect.set(l - mSelectionLeftPadding, t - mSelectionTopPadding, r - + mSelectionRightPadding, b + mSelectionBottomPadding); - } - @Override protected void dispatchDraw(Canvas canvas) { int saveCount = 0; @@ -3245,9 +3252,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te ((TransitionDrawable) d).resetTransition(); } } - if (d.supportsHotspots()) { - d.setHotspot(R.attr.state_pressed, x, y); - } + d.setHotspot(x, y); } if (longClickable) { @@ -3267,7 +3272,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } } - private boolean startScrollIfNeeded(int y, MotionEvent vtev) { + private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) { // Check if we have moved far enough that it looks more like a // scroll than a tap final int deltaY = y - mMotionY; @@ -3296,27 +3301,31 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } - scrollIfNeeded(y, vtev); + scrollIfNeeded(x, y, vtev); return true; } return false; } - private void scrollIfNeeded(int y, MotionEvent vtev) { + private void scrollIfNeeded(int x, int y, MotionEvent vtev) { int rawDeltaY = y - mMotionY; + int scrollOffsetCorrection = 0; + int scrollConsumedCorrection = 0; + if (mLastY == Integer.MIN_VALUE) { + rawDeltaY -= mMotionCorrection; + } if (dispatchNestedPreScroll(0, rawDeltaY, mScrollConsumed, mScrollOffset)) { rawDeltaY -= mScrollConsumed[1]; - mMotionCorrection -= mScrollOffset[1]; - if (mLastY != Integer.MIN_VALUE) { - mLastY -= mScrollOffset[1] + mScrollConsumed[1]; - } + scrollOffsetCorrection -= mScrollOffset[1]; + scrollConsumedCorrection -= mScrollConsumed[1]; if (vtev != null) { vtev.offsetLocation(0, mScrollOffset[1]); } } - final int deltaY = rawDeltaY - mMotionCorrection; - int incrementalDeltaY = mLastY != Integer.MIN_VALUE ? y - mLastY : deltaY; + final int deltaY = rawDeltaY; + int incrementalDeltaY = + mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY; int lastYCorrection = 0; if (mTouchMode == TOUCH_MODE_SCROLL) { @@ -3378,46 +3387,51 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te (motionViewRealTop - motionViewPrevTop); if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll, mScrollOffset)) { - mMotionCorrection -= mScrollOffset[1]; lastYCorrection -= mScrollOffset[1]; if (vtev != null) { vtev.offsetLocation(0, mScrollOffset[1]); } } else { - overScrollBy(0, overscroll, 0, mScrollY, 0, 0, - 0, mOverscrollDistance, true); - if (Math.abs(mOverscrollDistance) == Math.abs(mScrollY)) { - // Don't allow overfling if we're at the edge. - if (mVelocityTracker != null) { - mVelocityTracker.clear(); - } + final boolean atOverscrollEdge = overScrollBy(0, overscroll, + 0, mScrollY, 0, 0, 0, mOverscrollDistance, true); + + if (atOverscrollEdge && mVelocityTracker != null) { + // Don't allow overfling if we're at the edge + mVelocityTracker.clear(); } final int overscrollMode = getOverScrollMode(); if (overscrollMode == OVER_SCROLL_ALWAYS || (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { - mDirection = 0; // Reset when entering overscroll. - mTouchMode = TOUCH_MODE_OVERSCROLL; - if (deltaY > 0) { - mEdgeGlowTop.onPull((float) overscroll / getHeight()); + if (!atOverscrollEdge) { + mDirection = 0; // Reset when entering overscroll. + mTouchMode = TOUCH_MODE_OVERSCROLL; + } + if (incrementalDeltaY > 0) { + mEdgeGlowTop.onPull((float) -overscroll / getHeight(), + (float) x / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } - invalidate(mEdgeGlowTop.getBounds(false)); - } else if (deltaY < 0) { - mEdgeGlowBottom.onPull((float) overscroll / getHeight()); + invalidate(0, 0, getWidth(), + mEdgeGlowTop.getMaxHeight() + getPaddingTop()); + } else if (incrementalDeltaY < 0) { + mEdgeGlowBottom.onPull((float) overscroll / getHeight(), + 1.f - (float) x / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } - invalidate(mEdgeGlowBottom.getBounds(true)); + invalidate(0, getHeight() - getPaddingBottom() - + mEdgeGlowBottom.getMaxHeight(), getWidth(), + getHeight()); } } } } - mMotionY = y; + mMotionY = y + scrollOffsetCorrection; } - mLastY = y + lastYCorrection; + mLastY = y + lastYCorrection + scrollOffsetCorrection; } } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) { if (y != mLastY) { @@ -3445,17 +3459,22 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) { if (rawDeltaY > 0) { - mEdgeGlowTop.onPull((float) overScrollDistance / getHeight()); + mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(), + (float) x / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } - invalidate(mEdgeGlowTop.getBounds(false)); + invalidate(0, 0, getWidth(), + mEdgeGlowTop.getMaxHeight() + getPaddingTop()); } else if (rawDeltaY < 0) { - mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight()); + mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(), + 1.f - (float) x / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } - invalidate(mEdgeGlowBottom.getBounds(true)); + invalidate(0, getHeight() - getPaddingBottom() - + mEdgeGlowBottom.getMaxHeight(), getWidth(), + getHeight()); } } } @@ -3703,7 +3722,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te case TOUCH_MODE_DONE_WAITING: // Check if we have moved far enough that it looks more like a // scroll than a tap. If so, we'll enter scrolling mode. - if (startScrollIfNeeded(y, vtev)) { + if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) { break; } // Otherwise, check containment within list bounds. If we're @@ -3723,7 +3742,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te break; case TOUCH_MODE_SCROLL: case TOUCH_MODE_OVERSCROLL: - scrollIfNeeded(y, vtev); + scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev); break; } } @@ -3769,9 +3788,7 @@ 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()); - } + mSelector.setHotspot(x, ev.getY()); } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); @@ -3783,9 +3800,6 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); - if (mSelector != null && mSelector.supportsHotspots()) { - mSelector.removeHotspot(R.attr.state_pressed); - } if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) { performClick.run(); } @@ -4022,8 +4036,8 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te canvas.translate(leftPadding, edgeY); mEdgeGlowTop.setSize(width, getHeight()); if (mEdgeGlowTop.draw(canvas)) { - mEdgeGlowTop.setPosition(leftPadding, edgeY); - invalidate(mEdgeGlowTop.getBounds(false)); + invalidate(0, 0, getWidth(), + mEdgeGlowTop.getMaxHeight() + getPaddingTop()); } canvas.restoreToCount(restoreCount); } @@ -4040,9 +4054,9 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te canvas.rotate(180, width, 0); mEdgeGlowBottom.setSize(width, height); if (mEdgeGlowBottom.draw(canvas)) { - // Account for the rotation - mEdgeGlowBottom.setPosition(edgeX + width, edgeY); - invalidate(mEdgeGlowBottom.getBounds(true)); + invalidate(0, getHeight() - getPaddingBottom() - + mEdgeGlowBottom.getMaxHeight(), getWidth(), + getHeight()); } canvas.restoreToCount(restoreCount); } @@ -4161,7 +4175,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int y = (int) ev.getY(pointerIndex); initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(ev); - if (startScrollIfNeeded(y, null)) { + if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) { return true; } break; diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 225cd6d..43f623b 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -19,7 +19,9 @@ package android.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Insets; import android.graphics.Rect; +import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.util.AttributeSet; @@ -29,12 +31,13 @@ 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 final Rect mTempRect = new Rect(); + private Drawable mThumb; private int mThumbOffset; - + private boolean mSplitTrack; + /** * On touch, this offset plus the scaled value from the position of the * touch will form the progress value. Usually 0. @@ -51,10 +54,10 @@ public abstract class AbsSeekBar extends ProgressBar { * progress. */ private int mKeyProgressIncrement = 1; - + private static final int NO_ALPHA = 0xFF; private float mDisabledAlpha; - + private int mScaledTouchSlop; private float mTouchDownX; private boolean mIsDragging; @@ -76,12 +79,16 @@ public abstract class AbsSeekBar extends ProgressBar { 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 - int thumbOffset = a.getDimensionPixelOffset( + + final Drawable thumb = a.getDrawable(com.android.internal.R.styleable.SeekBar_thumb); + setThumb(thumb); + + // Guess thumb offset if thumb != null, but allow layout to override. + final int thumbOffset = a.getDimensionPixelOffset( com.android.internal.R.styleable.SeekBar_thumbOffset, getThumbOffset()); setThumbOffset(thumbOffset); + + mSplitTrack = a.getBoolean(com.android.internal.R.styleable.SeekBar_splitTrack, false); a.recycle(); a = context.obtainStyledAttributes(attrs, @@ -97,7 +104,7 @@ public abstract class AbsSeekBar extends ProgressBar { * <p> * If the thumb is a valid drawable (i.e. not null), half its width will be * used as the new thumb offset (@see #setThumbOffset(int)). - * + * * @param thumb Drawable representing the thumb */ public void setThumb(Drawable thumb) { @@ -132,7 +139,7 @@ public abstract class AbsSeekBar extends ProgressBar { mThumb = thumb; invalidate(); if (needUpdate) { - updateThumbPos(getWidth(), getHeight()); + updateThumbAndTrackPos(getWidth(), getHeight()); if (thumb != null && thumb.isStateful()) { // Note that if the states are different this won't work. // For now, let's consider that an app bug. @@ -162,7 +169,7 @@ public abstract class AbsSeekBar extends ProgressBar { /** * Sets the thumb offset that allows the thumb to extend out of the range of * the track. - * + * * @param thumbOffset The offset amount in pixels. */ public void setThumbOffset(int thumbOffset) { @@ -171,8 +178,27 @@ public abstract class AbsSeekBar extends ProgressBar { } /** + * Specifies whether the track should be split by the thumb. When true, + * the thumb's optical bounds will be clipped out of the track drawable, + * then the thumb will be drawn into the resulting gap. + * + * @param splitTrack Whether the track should be split by the thumb + */ + public void setSplitTrack(boolean splitTrack) { + mSplitTrack = splitTrack; + invalidate(); + } + + /** + * Returns whether the track should be split by the thumb. + */ + public boolean getSplitTrack() { + return mSplitTrack; + } + + /** * Sets the amount of progress changed via the arrow keys. - * + * * @param increment The amount to increment or decrement when the user * presses the arrow keys. */ @@ -184,14 +210,14 @@ public abstract class AbsSeekBar extends ProgressBar { * Returns the amount of progress changed via the arrow keys. * <p> * By default, this will be a value that is derived from the max progress. - * + * * @return The amount to increment or decrement when the user presses the * arrow keys. This will be positive. */ public int getKeyProgressIncrement() { return mKeyProgressIncrement; } - + @Override public synchronized void setMax(int max) { super.setMax(max); @@ -217,79 +243,95 @@ public abstract class AbsSeekBar extends ProgressBar { @Override protected void drawableStateChanged() { super.drawableStateChanged(); - - Drawable progressDrawable = getProgressDrawable(); + + final Drawable progressDrawable = getProgressDrawable(); if (progressDrawable != null) { progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha)); } - - if (mThumb != null && mThumb.isStateful()) { - int[] state = getDrawableState(); - mThumb.setState(state); + + final Drawable thumb = mThumb; + if (thumb != null && thumb.isStateful()) { + thumb.setState(getDrawableState()); } } - + + @Override + public void invalidateDrawable(Drawable dr) { + super.invalidateDrawable(dr); + + if (dr == mThumb) { + // Handle changes to thumb width and height. + requestLayout(); + } + } + @Override void onProgressRefresh(float scale, boolean fromUser) { super.onProgressRefresh(scale, fromUser); - Drawable thumb = mThumb; + + final Drawable thumb = mThumb; if (thumb != null) { setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); - /* - * Since we draw translated, the drawable's bounds that it signals - * for invalidation won't be the actual bounds we want invalidated, - * so just invalidate this whole view. - */ + + // Since we draw translated, the drawable's bounds that it signals + // for invalidation won't be the actual bounds we want invalidated, + // so just invalidate this whole view. invalidate(); } } - - + @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - updateThumbPos(w, h); + + updateThumbAndTrackPos(w, h); } - private void updateThumbPos(int w, int h) { - Drawable d = getCurrentDrawable(); - Drawable thumb = mThumb; - int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); + private void updateThumbAndTrackPos(int w, int h) { + final Drawable track = getCurrentDrawable(); + final Drawable thumb = mThumb; + // The max height does not incorporate padding, whereas the height - // parameter does - int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); - - int max = getMax(); - float scale = max > 0 ? (float) getProgress() / (float) max : 0; - + // parameter does. + final int trackHeight = Math.min(mMaxHeight, h - mPaddingTop - mPaddingBottom); + final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight(); + + // Apply offset to whichever item is taller. + final int trackOffset; + final int thumbOffset; if (thumbHeight > trackHeight) { - if (thumb != null) { - setThumbPos(w, thumb, scale, 0); - } - int gapForCenteringTrack = (thumbHeight - trackHeight) / 2; - if (d != null) { - // Canvas will be translated by the padding, so 0,0 is where we start drawing - d.setBounds(0, gapForCenteringTrack, - w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - gapForCenteringTrack - - mPaddingTop); - } + trackOffset = (thumbHeight - trackHeight) / 2; + thumbOffset = 0; } else { - if (d != null) { - // Canvas will be translated by the padding, so 0,0 is where we start drawing - d.setBounds(0, 0, w - mPaddingRight - mPaddingLeft, h - mPaddingBottom - - mPaddingTop); - } - int gap = (trackHeight - thumbHeight) / 2; - if (thumb != null) { - setThumbPos(w, thumb, scale, gap); - } + trackOffset = 0; + thumbOffset = (trackHeight - thumbHeight) / 2; + } + + if (track != null) { + track.setBounds(0, trackOffset, w - mPaddingRight - mPaddingLeft, + h - mPaddingBottom - trackOffset - mPaddingTop); + } + + if (thumb != null) { + setThumbPos(w, thumb, getScale(), thumbOffset); } } + private float getScale() { + final int max = getMax(); + return max > 0 ? getProgress() / (float) max : 0; + } + /** - * @param gap If set to {@link Integer#MIN_VALUE}, this will be ignored and + * Updates the thumb drawable bounds. + * + * @param w Width of the view, including padding + * @param thumb Drawable used for the thumb + * @param scale Current progress between 0 and 1 + * @param offset Vertical offset for centering. If set to + * {@link Integer#MIN_VALUE}, the current offset will be used. */ - private void setThumbPos(int w, Drawable thumb, float scale, int gap) { + private void setThumbPos(int w, Drawable thumb, float scale, int offset) { int available = w - mPaddingLeft - mPaddingRight; final int thumbWidth = thumb.getIntrinsicWidth(); final int thumbHeight = thumb.getIntrinsicHeight(); @@ -301,20 +343,20 @@ public abstract class AbsSeekBar extends ProgressBar { final int thumbPos = (int) (scale * available + 0.5f); final int top, bottom; - if (gap == Integer.MIN_VALUE) { + if (offset == Integer.MIN_VALUE) { final Rect oldBounds = thumb.getBounds(); top = oldBounds.top; bottom = oldBounds.bottom; } else { - top = gap; - bottom = gap + thumbHeight; + top = offset; + bottom = offset + thumbHeight; } final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos; final int right = left + thumbWidth; final Drawable background = getBackground(); - if (background != null && background.supportsHotspots()) { + if (background != null) { final Rect bounds = mThumb.getBounds(); final int offsetX = mPaddingLeft - mThumbOffset; final int offsetY = mPaddingTop; @@ -342,6 +384,33 @@ public abstract class AbsSeekBar extends ProgressBar { protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); + drawThumb(canvas); + } + + @Override + void drawTrack(Canvas canvas) { + final Drawable thumbDrawable = mThumb; + if (thumbDrawable != null && mSplitTrack) { + final Insets insets = thumbDrawable.getOpticalInsets(); + final Rect tempRect = mTempRect; + thumbDrawable.copyBounds(tempRect); + tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop); + tempRect.left += insets.left; + tempRect.right -= insets.right; + + final int saveCount = canvas.save(); + canvas.clipRect(tempRect, Op.DIFFERENCE); + super.drawTrack(canvas); + canvas.restoreToCount(saveCount); + } else { + super.drawTrack(canvas); + } + } + + /** + * Draw the thumb. + */ + void drawThumb(Canvas canvas) { if (mThumb != null) { canvas.save(); // Translate the padding. For the x, we need to allow the thumb to @@ -366,17 +435,17 @@ public abstract class AbsSeekBar extends ProgressBar { } dw += mPaddingLeft + mPaddingRight; dh += mPaddingTop + mPaddingBottom; - + setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0), resolveSizeAndState(dh, heightMeasureSpec, 0)); } - + @Override public boolean onTouchEvent(MotionEvent event) { if (!mIsUserSeekable || !isEnabled()) { return false; } - + switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (isInScrollingContainer()) { @@ -391,7 +460,7 @@ public abstract class AbsSeekBar extends ProgressBar { attemptClaimDrag(); } break; - + case MotionEvent.ACTION_MOVE: if (mIsDragging) { trackTouchEvent(event); @@ -408,7 +477,7 @@ public abstract class AbsSeekBar extends ProgressBar { } } break; - + case MotionEvent.ACTION_UP: if (mIsDragging) { trackTouchEvent(event); @@ -426,7 +495,7 @@ public abstract class AbsSeekBar extends ProgressBar { // value has not apparently changed) invalidate(); break; - + case MotionEvent.ACTION_CANCEL: if (mIsDragging) { onStopTrackingTouch(); @@ -438,17 +507,10 @@ public abstract class AbsSeekBar extends ProgressBar { return true; } - private void setHotspot(int id, float x, float y) { + private void setHotspot(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); + if (bg != null) { + bg.setHotspot(x, y); } } @@ -480,7 +542,7 @@ public abstract class AbsSeekBar extends ProgressBar { final int max = getMax(); progress += scale * max; - setHotspot(R.attr.state_pressed, x, (int) event.getY()); + setHotspot(x, (int) event.getY()); setProgress((int) progress, true); } @@ -493,7 +555,7 @@ public abstract class AbsSeekBar extends ProgressBar { mParent.requestDisallowInterceptTouchEvent(true); } } - + /** * This is called when the user has started touching this widget. */ @@ -506,7 +568,6 @@ public abstract class AbsSeekBar extends ProgressBar { * canceled. */ void onStopTrackingTouch() { - clearHotspot(R.attr.state_pressed); mIsDragging = false; } @@ -526,7 +587,7 @@ public abstract class AbsSeekBar extends ProgressBar { setProgress(progress - mKeyProgressIncrement, true); onKeyChange(); return true; - + case KeyEvent.KEYCODE_DPAD_RIGHT: if (progress >= getMax()) break; setProgress(progress + mKeyProgressIncrement, true); @@ -595,17 +656,13 @@ public abstract class AbsSeekBar extends ProgressBar { public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); - int max = getMax(); - float scale = max > 0 ? (float) getProgress() / (float) max : 0; - - Drawable thumb = mThumb; + final Drawable thumb = mThumb; if (thumb != null) { - setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE); - /* - * Since we draw translated, the drawable's bounds that it signals - * for invalidation won't be the actual bounds we want invalidated, - * so just invalidate this whole view. - */ + setThumbPos(getWidth(), thumb, getScale(), Integer.MIN_VALUE); + + // Since we draw translated, the drawable's bounds that it signals + // for invalidation won't be the actual bounds we want invalidated, + // so just invalidate this whole view. invalidate(); } } diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java index 51759c5..1fddf3e 100644 --- a/core/java/android/widget/ActionMenuPresenter.java +++ b/core/java/android/widget/ActionMenuPresenter.java @@ -544,6 +544,7 @@ public class ActionMenuPresenter extends BaseMenuPresenter public void setMenuView(ActionMenuView menuView) { mMenuView = menuView; + menuView.initialize(mMenu); } private static class SavedState implements Parcelable { diff --git a/core/java/android/widget/ActionMenuView.java b/core/java/android/widget/ActionMenuView.java index 3975edf..acee592 100644 --- a/core/java/android/widget/ActionMenuView.java +++ b/core/java/android/widget/ActionMenuView.java @@ -69,6 +69,7 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo /** @hide */ public void setPresenter(ActionMenuPresenter presenter) { mPresenter = presenter; + mPresenter.setMenuView(this); } @Override @@ -488,7 +489,7 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo @Override public void onDetachedFromWindow() { super.onDetachedFromWindow(); - mPresenter.dismissPopupMenus(); + dismissPopupMenus(); } /** @hide */ @@ -569,15 +570,65 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo mMenu = new MenuBuilder(context); mMenu.setCallback(new MenuBuilderCallback()); mPresenter = new ActionMenuPresenter(context); - mPresenter.setMenuView(this); mPresenter.setCallback(new ActionMenuPresenterCallback()); mMenu.addMenuPresenter(mPresenter); + mPresenter.setMenuView(this); } return mMenu; } /** + * Returns the current menu or null if one has not yet been configured. + * @hide Internal use only for action bar integration + */ + public MenuBuilder peekMenu() { + return mMenu; + } + + /** + * Show the overflow items from the associated menu. + * + * @return true if the menu was able to be shown, false otherwise + */ + public boolean showOverflowMenu() { + return mPresenter != null && mPresenter.showOverflowMenu(); + } + + /** + * Hide the overflow items from the associated menu. + * + * @return true if the menu was able to be hidden, false otherwise + */ + public boolean hideOverflowMenu() { + return mPresenter != null && mPresenter.hideOverflowMenu(); + } + + /** + * Check whether the overflow menu is currently showing. This may not reflect + * a pending show operation in progress. + * + * @return true if the overflow menu is currently showing + */ + public boolean isOverflowMenuShowing() { + return mPresenter != null && mPresenter.isOverflowMenuShowing(); + } + + /** @hide */ + public boolean isOverflowMenuShowPending() { + return mPresenter != null && mPresenter.isOverflowMenuShowPending(); + } + + /** + * Dismiss any popups associated with this menu view. + */ + public void dismissPopupMenus() { + if (mPresenter != null) { + mPresenter.dismissPopupMenus(); + } + } + + /** * @hide Private LinearLayout (superclass) API. Un-hide if LinearLayout API is made public. */ @Override @@ -601,6 +652,11 @@ public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvo return false; } + /** @hide */ + public void setExpandedActionViewsExclusive(boolean exclusive) { + mPresenter.setExpandedActionViewsExclusive(exclusive); + } + /** * Interface responsible for receiving menu item click events if the items themselves * do not have individual item click listeners. diff --git a/core/java/android/widget/CheckedTextView.java b/core/java/android/widget/CheckedTextView.java index 1533510..3ae9508 100644 --- a/core/java/android/widget/CheckedTextView.java +++ b/core/java/android/widget/CheckedTextView.java @@ -24,6 +24,7 @@ import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.view.Gravity; +import android.view.RemotableViewMethod; import android.view.ViewDebug; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; @@ -156,10 +157,36 @@ public class CheckedTextView extends TextView implements Checkable { mCheckMarkWidth = 0; } mCheckMarkDrawable = d; - // Do padding resolution. This will call internalSetPadding() and do a requestLayout() if needed. + + // Do padding resolution. This will call internalSetPadding() and do a + // requestLayout() if needed. resolvePadding(); } + @RemotableViewMethod + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + + if (mCheckMarkDrawable != null) { + mCheckMarkDrawable.setVisible(visibility == VISIBLE, false); + } + } + + @Override + public void jumpDrawablesToCurrentState() { + super.jumpDrawablesToCurrentState(); + + if (mCheckMarkDrawable != null) { + mCheckMarkDrawable.jumpToCurrentState(); + } + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return who == mCheckMarkDrawable || super.verifyDrawable(who); + } + /** * Gets the checkmark drawable * @@ -249,6 +276,11 @@ public class CheckedTextView extends TextView implements Checkable { } checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom); checkMarkDrawable.draw(canvas); + + final Drawable background = getBackground(); + if (background != null) { + background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom); + } } } diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java index 9e17cca..6aff4f4 100644 --- a/core/java/android/widget/CompoundButton.java +++ b/core/java/android/widget/CompoundButton.java @@ -285,7 +285,7 @@ public abstract class CompoundButton extends Button implements Checkable { buttonDrawable.setBounds(left, top, right, bottom); final Drawable background = getBackground(); - if (background != null && background.supportsHotspots()) { + if (background != null) { background.setHotspotBounds(left, top, right, bottom); } } diff --git a/core/java/android/widget/DatePicker.java b/core/java/android/widget/DatePicker.java index 265dbcd..2c1a77c 100644 --- a/core/java/android/widget/DatePicker.java +++ b/core/java/android/widget/DatePicker.java @@ -24,6 +24,7 @@ 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; @@ -814,8 +815,7 @@ public class DatePicker extends FrameLayout { 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()); + String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd"); char[] order = ICU.getDateFormatOrder(pattern); final int spinnerCount = order.length; for (int i = 0; i < spinnerCount; i++) { diff --git a/core/java/android/widget/EdgeEffect.java b/core/java/android/widget/EdgeEffect.java index fa37443..c4a40b4 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -16,13 +16,16 @@ package android.widget; +import android.content.res.TypedArray; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import com.android.internal.R; +import android.graphics.RectF; import android.content.Context; -import android.content.res.Resources; import android.graphics.Canvas; -import android.graphics.drawable.Drawable; +import android.util.FloatMath; import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.Interpolator; @@ -55,16 +58,11 @@ public class EdgeEffect { // Time it will take before a pulled glow begins receding in ms private static final int PULL_TIME = 167; - // Time it will take in ms for a pulled glow to decay to partial strength before release - private static final int PULL_DECAY_TIME = 1000; - private static final float MAX_ALPHA = 1.f; - private static final float HELD_EDGE_SCALE_Y = 0.5f; - private static final float MAX_GLOW_HEIGHT = 4.f; + private static final float MAX_GLOW_SCALE = 2.f; - private static final float PULL_GLOW_BEGIN = 1.f; - private static final float PULL_EDGE_BEGIN = 0.6f; + private static final float PULL_GLOW_BEGIN = 0.f; // Minimum velocity that will be absorbed private static final int MIN_VELOCITY = 100; @@ -73,24 +71,13 @@ public class EdgeEffect { private static final float EPSILON = 0.001f; - private final Drawable mEdge; - private final Drawable mGlow; - private int mWidth; - private int mHeight; - private int mX; - private int mY; - private static final int MIN_WIDTH = 300; - private final int mMinWidth; - - private float mEdgeAlpha; - private float mEdgeScaleY; + private static final double ANGLE = Math.PI / 6; + private static final float SIN = (float) Math.sin(ANGLE); + private static final float COS = (float) Math.cos(ANGLE); + private float mGlowAlpha; private float mGlowScaleY; - private float mEdgeAlphaStart; - private float mEdgeAlphaFinish; - private float mEdgeScaleYStart; - private float mEdgeScaleYFinish; private float mGlowAlphaStart; private float mGlowAlphaFinish; private float mGlowScaleYStart; @@ -107,16 +94,11 @@ public class EdgeEffect { private static final int STATE_RECEDE = 3; private static final int STATE_PULL_DECAY = 4; - // How much dragging should effect the height of the edge image. - // Number determined by user testing. - private static final int PULL_DISTANCE_EDGE_FACTOR = 7; - // How much dragging should effect the height of the glow image. // Number determined by user testing. private static final int PULL_DISTANCE_GLOW_FACTOR = 7; private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 1.1f; - private static final int VELOCITY_EDGE_FACTOR = 8; private static final int VELOCITY_GLOW_FACTOR = 12; private int mState = STATE_IDLE; @@ -124,30 +106,27 @@ public class EdgeEffect { private float mPullDistance; private final Rect mBounds = new Rect(); - - private final int mEdgeHeight; - private final int mGlowHeight; - private final int mGlowWidth; - private final int mMaxEffectHeight; + private final RectF mArcRect = new RectF(); + private final Paint mPaint = new Paint(); + private float mRadius; + private float mBaseGlowHeight; + private float mDisplacement = 0.5f; + private float mTargetDisplacement = 0.5f; /** * Construct a new EdgeEffect with a theme appropriate for the provided context. * @param context Context used to provide theming and resource information for the EdgeEffect */ public EdgeEffect(Context context) { - final Resources res = context.getResources(); - mEdge = context.getDrawable(R.drawable.overscroll_edge); - mGlow = context.getDrawable(R.drawable.overscroll_glow); - - mEdgeHeight = mEdge.getIntrinsicHeight(); - mGlowHeight = mGlow.getIntrinsicHeight(); - mGlowWidth = mGlow.getIntrinsicWidth(); - - mMaxEffectHeight = (int) (Math.min( - mGlowHeight * MAX_GLOW_HEIGHT * mGlowHeight / mGlowWidth * 0.6f, - mGlowHeight * MAX_GLOW_HEIGHT) + 0.5f); - - mMinWidth = (int) (res.getDisplayMetrics().density * MIN_WIDTH + 0.5f); + mPaint.setAntiAlias(true); + final TypedArray a = context.obtainStyledAttributes( + com.android.internal.R.styleable.EdgeEffect); + final int themeColor = a.getColor( + com.android.internal.R.styleable.EdgeEffect_colorPrimaryLight, 0xff666666); + a.recycle(); + mPaint.setColor((themeColor & 0xffffff) | 0x33000000); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP)); mInterpolator = new DecelerateInterpolator(); } @@ -158,20 +137,13 @@ public class EdgeEffect { * @param height Effect height in pixels */ public void setSize(int width, int height) { - mWidth = width; - mHeight = height; - } + final float r = width * 0.75f / SIN; + final float y = COS * r; + final float h = r - y; + mRadius = r; + mBaseGlowHeight = h; - /** - * Set the position of this edge effect in pixels. This position is - * only used by {@link #getBounds(boolean)}. - * - * @param x The position of the edge effect on the X axis - * @param y The position of the edge effect on the Y axis - */ - void setPosition(int x, int y) { - mX = x; - mY = y; + mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h)); } /** @@ -199,17 +171,38 @@ public class EdgeEffect { * The host view should always {@link android.view.View#invalidate()} after this * and draw the results accordingly. * + * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement + * of the pull point is known.</p> + * * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to * 1.f (full length of the view) or negative values to express change * back toward the edge reached to initiate the effect. */ public void onPull(float deltaDistance) { + onPull(deltaDistance, 0.5f); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} after this + * and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + */ + public void onPull(float deltaDistance, float displacement) { final long now = AnimationUtils.currentAnimationTimeMillis(); + mTargetDisplacement = displacement; if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) { return; } if (mState != STATE_PULL) { - mGlowScaleY = PULL_GLOW_BEGIN; + mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY); } mState = STATE_PULL; @@ -217,30 +210,20 @@ public class EdgeEffect { mDuration = PULL_TIME; mPullDistance += deltaDistance; - float distance = Math.abs(mPullDistance); - - mEdgeAlpha = mEdgeAlphaStart = Math.max(PULL_EDGE_BEGIN, Math.min(distance, MAX_ALPHA)); - mEdgeScaleY = mEdgeScaleYStart = Math.max( - HELD_EDGE_SCALE_Y, Math.min(distance * PULL_DISTANCE_EDGE_FACTOR, 1.f)); + final float absdd = Math.abs(deltaDistance); mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA, - mGlowAlpha + - (Math.abs(deltaDistance) * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); + mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR)); - float glowChange = Math.abs(deltaDistance); - if (deltaDistance > 0 && mPullDistance < 0) { - glowChange = -glowChange; - } if (mPullDistance == 0) { - mGlowScaleY = 0; - } + mGlowScaleY = mGlowScaleYStart = 0; + } else { + final float scale = Math.max(0, 1 - 1 / + FloatMath.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3f) / 0.7f; - // Do not allow glow to get larger than MAX_GLOW_HEIGHT. - mGlowScaleY = mGlowScaleYStart = Math.min(MAX_GLOW_HEIGHT, Math.max( - 0, mGlowScaleY + glowChange * PULL_DISTANCE_GLOW_FACTOR)); + mGlowScaleY = mGlowScaleYStart = scale; + } - mEdgeAlphaFinish = mEdgeAlpha; - mEdgeScaleYFinish = mEdgeScaleY; mGlowAlphaFinish = mGlowAlpha; mGlowScaleYFinish = mGlowScaleY; } @@ -259,13 +242,9 @@ public class EdgeEffect { } mState = STATE_RECEDE; - mEdgeAlphaStart = mEdgeAlpha; - mEdgeScaleYStart = mEdgeScaleY; mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; - mEdgeAlphaFinish = 0.f; - mEdgeScaleYFinish = 0.f; mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; @@ -290,30 +269,21 @@ public class EdgeEffect { mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = 0.15f + (velocity * 0.02f); - // The edge should always be at least partially visible, regardless - // of velocity. - mEdgeAlphaStart = 0.f; - mEdgeScaleY = mEdgeScaleYStart = 0.f; // The glow depends more on the velocity, and therefore starts out // nearly invisible. mGlowAlphaStart = 0.3f; - mGlowScaleYStart = 0.f; + mGlowScaleYStart = Math.max(mGlowScaleY, 0.f); - // Factor the velocity by 8. Testing on device shows this works best to - // reflect the strength of the user's scrolling. - mEdgeAlphaFinish = Math.max(0, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1)); - // Edge should never get larger than the size of its asset. - mEdgeScaleYFinish = Math.max( - HELD_EDGE_SCALE_Y, Math.min(velocity * VELOCITY_EDGE_FACTOR, 1.f)); // Growth for the size of the glow should be quadratic to properly // respond // to a user's scrolling speed. The faster the scrolling speed, the more // intense the effect should be for both the size and the saturation. - mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f), 1.75f); + mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f); // Alpha should change for the glow as well as size. mGlowAlphaFinish = Math.max( mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA)); + mTargetDisplacement = 0.5f; } @@ -330,52 +300,40 @@ public class EdgeEffect { public boolean draw(Canvas canvas) { update(); - mGlow.setAlpha((int) (Math.max(0, Math.min(mGlowAlpha, 1)) * 255)); + final int count = canvas.save(); - int glowBottom = (int) Math.min( - mGlowHeight * mGlowScaleY * mGlowHeight / mGlowWidth * 0.6f, - mGlowHeight * MAX_GLOW_HEIGHT); - if (mWidth < mMinWidth) { - // Center the glow and clip it. - int glowLeft = (mWidth - mMinWidth)/2; - mGlow.setBounds(glowLeft, 0, mWidth - glowLeft, glowBottom); - } else { - // Stretch the glow to fit. - mGlow.setBounds(0, 0, mWidth, glowBottom); - } + final float y = mBounds.height(); + final float centerY = y - mRadius; + final float centerX = mBounds.centerX(); - mGlow.draw(canvas); + mArcRect.set(centerX - mRadius, centerY - mRadius, centerX + mRadius, centerY + mRadius); + canvas.scale(1.f, Math.min(mGlowScaleY, 1.f), centerX, 0); - mEdge.setAlpha((int) (Math.max(0, Math.min(mEdgeAlpha, 1)) * 255)); + final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; + float translateX = mBounds.width() * displacement / 2; - int edgeBottom = (int) (mEdgeHeight * mEdgeScaleY); - if (mWidth < mMinWidth) { - // Center the edge and clip it. - int edgeLeft = (mWidth - mMinWidth)/2; - mEdge.setBounds(edgeLeft, 0, mWidth - edgeLeft, edgeBottom); - } else { - // Stretch the edge to fit. - mEdge.setBounds(0, 0, mWidth, edgeBottom); - } - mEdge.draw(canvas); + canvas.clipRect(Float.MIN_VALUE, mBounds.top, + Float.MAX_VALUE, Float.MAX_VALUE); + canvas.translate(translateX, 0); + canvas.drawArc(mArcRect, 45, 90, true, mPaint); + canvas.restoreToCount(count); - if (mState == STATE_RECEDE && glowBottom == 0 && edgeBottom == 0) { + boolean oneLastFrame = false; + if (mState == STATE_RECEDE && mGlowScaleY == 0) { mState = STATE_IDLE; + oneLastFrame = true; } - return mState != STATE_IDLE; + return mState != STATE_IDLE || oneLastFrame; } /** - * Returns the bounds of the edge effect. - * - * @hide + * Return the maximum height that the edge effect will be drawn at given the original + * {@link #setSize(int, int) input size}. + * @return The maximum height of the edge effect */ - public Rect getBounds(boolean reverse) { - mBounds.set(0, 0, mWidth, mMaxEffectHeight); - mBounds.offset(mX, mY - (reverse ? mMaxEffectHeight : 0)); - - return mBounds; + public int getMaxHeight() { + return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f); } private void update() { @@ -384,10 +342,9 @@ public class EdgeEffect { final float interp = mInterpolator.getInterpolation(t); - mEdgeAlpha = mEdgeAlphaStart + (mEdgeAlphaFinish - mEdgeAlphaStart) * interp; - mEdgeScaleY = mEdgeScaleYStart + (mEdgeScaleYFinish - mEdgeScaleYStart) * interp; mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp; mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp; + mDisplacement = (mDisplacement + mTargetDisplacement) / 2; if (t >= 1.f - EPSILON) { switch (mState) { @@ -396,42 +353,17 @@ public class EdgeEffect { mStartTime = AnimationUtils.currentAnimationTimeMillis(); mDuration = RECEDE_TIME; - mEdgeAlphaStart = mEdgeAlpha; - mEdgeScaleYStart = mEdgeScaleY; mGlowAlphaStart = mGlowAlpha; mGlowScaleYStart = mGlowScaleY; - // After absorb, the glow and edge should fade to nothing. - mEdgeAlphaFinish = 0.f; - mEdgeScaleYFinish = 0.f; + // After absorb, the glow should fade to nothing. mGlowAlphaFinish = 0.f; mGlowScaleYFinish = 0.f; break; case STATE_PULL: - mState = STATE_PULL_DECAY; - mStartTime = AnimationUtils.currentAnimationTimeMillis(); - mDuration = PULL_DECAY_TIME; - - mEdgeAlphaStart = mEdgeAlpha; - mEdgeScaleYStart = mEdgeScaleY; - mGlowAlphaStart = mGlowAlpha; - mGlowScaleYStart = mGlowScaleY; - - // After pull, the glow and edge should fade to nothing. - mEdgeAlphaFinish = 0.f; - mEdgeScaleYFinish = 0.f; - mGlowAlphaFinish = 0.f; - mGlowScaleYFinish = 0.f; + // Hold in this state until explicitly released. break; case STATE_PULL_DECAY: - // When receding, we want edge to decrease more slowly - // than the glow. - float factor = mGlowScaleYFinish != 0 ? 1 - / (mGlowScaleYFinish * mGlowScaleYFinish) - : Float.MAX_VALUE; - mEdgeScaleY = mEdgeScaleYStart + - (mEdgeScaleYFinish - mEdgeScaleYStart) * - interp * factor; mState = STATE_RECEDE; break; case STATE_RECEDE: diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index b0a4e24..27d6b82 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -39,6 +39,7 @@ import android.content.pm.PackageManager; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; @@ -94,6 +95,8 @@ import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.WindowManager; import android.view.inputmethod.CorrectionInfo; +import android.view.inputmethod.CursorAnchorInfo; +import android.view.inputmethod.CursorAnchorInfo.CursorAnchorInfoBuilder; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -215,6 +218,8 @@ public class Editor { private TextView mTextView; + final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier(); + Editor(TextView textView) { mTextView = textView; } @@ -249,9 +254,13 @@ public class Editor { // We had an active selection from before, start the selection mode. startSelectionActionMode(); } + + getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); } void onDetachedFromWindow() { + getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier); + if (mError != null) { hideError(); } @@ -780,7 +789,7 @@ public class Editor { boolean parentPositionChanged, boolean parentScrolled); } - private boolean isPositionVisible(int positionX, int positionY) { + private boolean isPositionVisible(final float positionX, final float positionY) { synchronized (TEMP_POSITION) { final float[] position = TEMP_POSITION; position[0] = positionX; @@ -2134,7 +2143,8 @@ public class Editor { private class PositionListener implements ViewTreeObserver.OnPreDrawListener { // 3 handles // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) - private final int MAXIMUM_NUMBER_OF_LISTENERS = 6; + // 1 CursorAnchorInfoNotifier + private final int MAXIMUM_NUMBER_OF_LISTENERS = 7; private TextViewPositionListener[] mPositionListeners = new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; @@ -2997,6 +3007,122 @@ public class Editor { } } + /** + * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} + * while the input method is requesting the cursor/anchor position. Does nothing as long as + * {@link InputMethodManager#isWatchingCursor(View)} returns false. + */ + private final class CursorAnchorInfoNotifier implements TextViewPositionListener { + final CursorAnchorInfoBuilder mSelectionInfoBuilder = new CursorAnchorInfoBuilder(); + final int[] mTmpIntOffset = new int[2]; + final Matrix mViewToScreenMatrix = new Matrix(); + + @Override + public void updatePosition(int parentPositionX, int parentPositionY, + boolean parentPositionChanged, boolean parentScrolled) { + final InputMethodState ims = mInputMethodState; + if (ims == null || ims.mBatchEditNesting > 0) { + return; + } + final InputMethodManager imm = InputMethodManager.peekInstance(); + if (null == imm) { + return; + } + // Skip if the IME has not requested the cursor/anchor position. + if (!imm.isWatchingCursor(mTextView)) { + return; + } + Layout layout = mTextView.getLayout(); + if (layout == null) { + return; + } + + final CursorAnchorInfoBuilder builder = mSelectionInfoBuilder; + builder.reset(); + + final int selectionStart = mTextView.getSelectionStart(); + builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd()); + + // Construct transformation matrix from view local coordinates to screen coordinates. + mViewToScreenMatrix.set(mTextView.getMatrix()); + mTextView.getLocationOnScreen(mTmpIntOffset); + mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]); + builder.setMatrix(mViewToScreenMatrix); + + final float viewportToContentHorizontalOffset = + mTextView.viewportToContentHorizontalOffset(); + final float viewportToContentVerticalOffset = + mTextView.viewportToContentVerticalOffset(); + + final CharSequence text = mTextView.getText(); + if (text instanceof Spannable) { + final Spannable sp = (Spannable) text; + int composingTextStart = EditableInputConnection.getComposingSpanStart(sp); + int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp); + if (composingTextEnd < composingTextStart) { + final int temp = composingTextEnd; + composingTextEnd = composingTextStart; + composingTextStart = temp; + } + final boolean hasComposingText = + (0 <= composingTextStart) && (composingTextStart < composingTextEnd); + if (hasComposingText) { + final CharSequence composingText = text.subSequence(composingTextStart, + composingTextEnd); + builder.setComposingText(composingTextStart, composingText); + } + for (int offset = composingTextStart; offset < composingTextEnd; offset++) { + if (offset < 0) { + continue; + } + final int line = layout.getLineForOffset(offset); + final float left = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float top = layout.getLineTop(line) + viewportToContentVerticalOffset; + // Here we are tentatively passing offset + 1 to calculate the other side of + // the primary horizontal to preserve as many positions as possible so that + // the IME can reconstruct the layout entirely. However, we should revisit this + // to have a clear specification about the relationship between the index of + // the character and its bounding box. See also the TODO comment below. + final float right = layout.getPrimaryHorizontal(offset + 1) + + viewportToContentHorizontalOffset; + final float bottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + // Take TextView's padding and scroll into account. + if (isPositionVisible(left, top) && isPositionVisible(right, bottom)) { + // Here offset is the index in Java chars. + // TODO: We must have a well-defined specification. For example, how + // RTL, surrogate pairs, and composition letters are handled must be + // documented. + builder.addCharacterRect(offset, left, top, right, bottom); + } + } + } + + // Treat selectionStart as the insertion point. + if (0 <= selectionStart) { + final int offset = selectionStart; + final int line = layout.getLineForOffset(offset); + final float insertionMarkerX = layout.getPrimaryHorizontal(offset) + + viewportToContentHorizontalOffset; + final float insertionMarkerTop = layout.getLineTop(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBaseline = layout.getLineBaseline(line) + + viewportToContentVerticalOffset; + final float insertionMarkerBottom = layout.getLineBottom(line) + + viewportToContentVerticalOffset; + // Take TextView's padding and scroll into account. + if (isPositionVisible(insertionMarkerX, insertionMarkerTop) && + isPositionVisible(insertionMarkerX, insertionMarkerBottom)) { + builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, + insertionMarkerBaseline, insertionMarkerBottom); + } + } + + imm.updateCursorAnchorInfo(mTextView, builder.build()); + } + } + private abstract class HandleView extends View implements TextViewPositionListener { protected Drawable mDrawable; protected Drawable mDrawableLtr; diff --git a/core/java/android/widget/GridLayout.java b/core/java/android/widget/GridLayout.java index 8511601..defc26c 100644 --- a/core/java/android/widget/GridLayout.java +++ b/core/java/android/widget/GridLayout.java @@ -104,14 +104,16 @@ import static java.lang.Math.min; * * <h4>Excess Space Distribution</h4> * - * GridLayout's distribution of excess space is based on <em>priority</em> - * rather than <em>weight</em>. + * As of API 21, GridLayout's distribution of excess space accomodates the principle of weight. + * In the event that no weights are specified, the previous conventions are respected and + * columns and rows are taken as flexible if their views specify some form of alignment + * within their groups. * <p> - * A child's ability to stretch is inferred from the alignment properties of - * its row and column groups (which are typically set by setting the - * {@link LayoutParams#setGravity(int) gravity} property of the child's layout parameters). - * If alignment was defined along a given axis then the component - * is taken as <em>flexible</em> in that direction. If no alignment was set, + * The flexibility of a view is therefore influenced by its alignment which is, + * in turn, typically defined by setting the + * {@link LayoutParams#setGravity(int) gravity} property of the child's layout parameters. + * If either a weight or alignment were defined along a given axis then the component + * is taken as <em>flexible</em> in that direction. If no weight or alignment was set, * the component is instead assumed to be <em>inflexible</em>. * <p> * Multiple components in the same row or column group are @@ -122,12 +124,16 @@ import static java.lang.Math.min; * elements is flexible if <em>one</em> of its elements is flexible. * <p> * To make a column stretch, make sure all of the components inside it define a - * gravity. To prevent a column from stretching, ensure that one of the components - * in the column does not define a gravity. + * weight or a gravity. To prevent a column from stretching, ensure that one of the components + * in the column does not define a weight or a gravity. * <p> * When the principle of flexibility does not provide complete disambiguation, * GridLayout's algorithms favour rows and columns that are closer to its <em>right</em> - * and <em>bottom</em> edges. + * and <em>bottom</em> edges. To be more precise, GridLayout treats each of its layout + * parameters as a constraint in the a set of variables that define the grid-lines along a + * given axis. During layout, GridLayout solves the constraints so as to return the unique + * solution to those constraints for which all variables are less-than-or-equal-to + * the corresponding value in any other valid solution. * * <h4>Interpretation of GONE</h4> * @@ -140,18 +146,6 @@ import static java.lang.Math.min; * had never been added to it. * These statements apply equally to rows as well as columns, and to groups of rows or columns. * - * <h5>Limitations</h5> - * - * GridLayout does not provide support for the principle of <em>weight</em>, as defined in - * {@link LinearLayout.LayoutParams#weight}. In general, it is not therefore possible - * to configure a GridLayout to distribute excess space between multiple components. - * <p> - * Some common use-cases may nevertheless be accommodated as follows. - * To place equal amounts of space around a component in a cell group; - * use {@link #CENTER} alignment (or {@link LayoutParams#setGravity(int) gravity}). - * For complete control over excess space distribution in a row or column; - * use a {@link LinearLayout} subview to hold the components in the associated cell group. - * When using either of these techniques, bear in mind that cell groups may be defined to overlap. * <p> * See {@link GridLayout.LayoutParams} for a full description of the * layout parameters used by GridLayout. @@ -1018,6 +1012,8 @@ public class GridLayout extends ViewGroup { LayoutParams lp = getLayoutParams(c); if (firstPass) { measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height); + mHorizontalAxis.recordOriginalMeasurement(i); + mVerticalAxis.recordOriginalMeasurement(i); } else { boolean horizontal = (mOrientation == HORIZONTAL); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; @@ -1245,6 +1241,11 @@ public class GridLayout extends ViewGroup { public int[] locations; public boolean locationsValid = false; + public boolean hasWeights; + public boolean hasWeightsValid = false; + public int[] originalMeasurements; + public int[] deltas; + boolean orderPreserved = DEFAULT_ORDER_PRESERVED; private MutableInt parentMin = new MutableInt(0); @@ -1321,7 +1322,10 @@ public class GridLayout extends ViewGroup { // we must include views that are GONE here, see introductory javadoc LayoutParams lp = getLayoutParams(c); Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; - groupBounds.getValue(i).include(GridLayout.this, c, spec, this); + int size = (spec.weight == 0) ? + getMeasurementIncludingMargin(c, horizontal) : + getOriginalMeasurements()[i] + getDeltas()[i]; + groupBounds.getValue(i).include(GridLayout.this, c, spec, this, size); } } @@ -1693,8 +1697,94 @@ public class GridLayout extends ViewGroup { return trailingMargins; } - private void computeLocations(int[] a) { + private void solve(int[] a) { solve(getArcs(), a); + } + + private boolean computeHasWeights() { + for (int i = 0, N = getChildCount(); i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + if (spec.weight != 0) { + return true; + } + } + return false; + } + + private boolean hasWeights() { + if (!hasWeightsValid) { + hasWeights = computeHasWeights(); + hasWeightsValid = true; + } + return hasWeights; + } + + public int[] getOriginalMeasurements() { + if (originalMeasurements == null) { + originalMeasurements = new int[getChildCount()]; + } + return originalMeasurements; + } + + private void recordOriginalMeasurement(int i) { + if (hasWeights()) { + getOriginalMeasurements()[i] = getMeasurementIncludingMargin(getChildAt(i), horizontal); + } + } + + public int[] getDeltas() { + if (deltas == null) { + deltas = new int[getChildCount()]; + } + return deltas; + } + + private void shareOutDelta() { + int totalDelta = 0; + float totalWeight = 0; + for (int i = 0, N = getChildCount(); i < N; i++) { + View c = getChildAt(i); + LayoutParams lp = getLayoutParams(c); + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + float weight = spec.weight; + if (weight != 0) { + int delta = getMeasurement(c, horizontal) - getOriginalMeasurements()[i]; + totalDelta += delta; + totalWeight += weight; + } + } + for (int i = 0, N = getChildCount(); i < N; i++) { + LayoutParams lp = getLayoutParams(getChildAt(i)); + Spec spec = horizontal ? lp.columnSpec : lp.rowSpec; + float weight = spec.weight; + if (weight != 0) { + int delta = Math.round((weight * totalDelta / totalWeight)); + deltas[i] = delta; + // the two adjustments below are to counter the above rounding and avoid off-by-ones at the end + totalDelta -= delta; + totalWeight -= weight; + } + } + } + + private void solveAndDistributeSpace(int[] a) { + Arrays.fill(getDeltas(), 0); + solve(a); + shareOutDelta(); + arcsValid = false; + forwardLinksValid = false; + backwardLinksValid = false; + groupBoundsValid = false; + solve(a); + } + + private void computeLocations(int[] a) { + if (!hasWeights()) { + solve(a); + } else { + solveAndDistributeSpace(a); + } if (!orderPreserved) { // Solve returns the smallest solution to the constraint system for which all // values are positive. One value is therefore zero - though if the row/col @@ -1777,6 +1867,10 @@ public class GridLayout extends ViewGroup { locations = null; + originalMeasurements = null; + deltas = null; + hasWeightsValid = false; + invalidateValues(); } @@ -1810,6 +1904,9 @@ public class GridLayout extends ViewGroup { * both aspects of alignment within the cell group. It is also possible to specify a child's * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)} * method. + * <p> + * The weight property is also included in Spec and specifies the proportion of any + * excess space that is due to the associated view. * * <h4>WRAP_CONTENT and MATCH_PARENT</h4> * @@ -1851,9 +1948,11 @@ public class GridLayout extends ViewGroup { * <li>{@link #rowSpec}<code>.row</code> = {@link #UNDEFINED} </li> * <li>{@link #rowSpec}<code>.rowSpan</code> = 1 </li> * <li>{@link #rowSpec}<code>.alignment</code> = {@link #BASELINE} </li> + * <li>{@link #rowSpec}<code>.weight</code> = 0 </li> * <li>{@link #columnSpec}<code>.column</code> = {@link #UNDEFINED} </li> * <li>{@link #columnSpec}<code>.columnSpan</code> = 1 </li> * <li>{@link #columnSpec}<code>.alignment</code> = {@link #START} </li> + * <li>{@link #columnSpec}<code>.weight</code> = 0 </li> * </ul> * * See {@link GridLayout} for a more complete description of the conventions @@ -1861,8 +1960,10 @@ public class GridLayout extends ViewGroup { * * @attr ref android.R.styleable#GridLayout_Layout_layout_row * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan + * @attr ref android.R.styleable#GridLayout_Layout_layout_rowWeight * @attr ref android.R.styleable#GridLayout_Layout_layout_column * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan + * @attr ref android.R.styleable#GridLayout_Layout_layout_columnWeight * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity */ public static class LayoutParams extends MarginLayoutParams { @@ -1889,9 +1990,11 @@ public class GridLayout extends ViewGroup { private static final int COLUMN = R.styleable.GridLayout_Layout_layout_column; private static final int COLUMN_SPAN = R.styleable.GridLayout_Layout_layout_columnSpan; + private static final int COLUMN_WEIGHT = R.styleable.GridLayout_Layout_layout_columnWeight; private static final int ROW = R.styleable.GridLayout_Layout_layout_row; private static final int ROW_SPAN = R.styleable.GridLayout_Layout_layout_rowSpan; + private static final int ROW_WEIGHT = R.styleable.GridLayout_Layout_layout_rowWeight; private static final int GRAVITY = R.styleable.GridLayout_Layout_layout_gravity; @@ -2034,11 +2137,13 @@ public class GridLayout extends ViewGroup { int column = a.getInt(COLUMN, DEFAULT_COLUMN); int colSpan = a.getInt(COLUMN_SPAN, DEFAULT_SPAN_SIZE); - this.columnSpec = spec(column, colSpan, getAlignment(gravity, true)); + float colWeight = a.getFloat(COLUMN_WEIGHT, Spec.DEFAULT_WEIGHT); + this.columnSpec = spec(column, colSpan, getAlignment(gravity, true), colWeight); int row = a.getInt(ROW, DEFAULT_ROW); int rowSpan = a.getInt(ROW_SPAN, DEFAULT_SPAN_SIZE); - this.rowSpec = spec(row, rowSpan, getAlignment(gravity, false)); + float rowWeight = a.getFloat(ROW_WEIGHT, Spec.DEFAULT_WEIGHT); + this.rowSpec = spec(row, rowSpan, getAlignment(gravity, false), rowWeight); } finally { a.recycle(); } @@ -2273,10 +2378,9 @@ public class GridLayout extends ViewGroup { return before - a.getAlignmentValue(c, size, gl.getLayoutMode()); } - protected final void include(GridLayout gl, View c, Spec spec, Axis axis) { + protected final void include(GridLayout gl, View c, Spec spec, Axis axis, int size) { this.flexibility &= spec.getFlexibility(); boolean horizontal = axis.horizontal; - int size = gl.getMeasurementIncludingMargin(c, horizontal); Alignment alignment = gl.getAlignment(spec.alignment, horizontal); // todo test this works correctly when the returned value is UNDEFINED int before = alignment.getAlignmentValue(c, size, gl.getLayoutMode()); @@ -2401,36 +2505,43 @@ public class GridLayout extends ViewGroup { * <li>{@link #spec(int, int)}</li> * <li>{@link #spec(int, Alignment)}</li> * <li>{@link #spec(int, int, Alignment)}</li> + * <li>{@link #spec(int, float)}</li> + * <li>{@link #spec(int, int, float)}</li> + * <li>{@link #spec(int, Alignment, float)}</li> + * <li>{@link #spec(int, int, Alignment, float)}</li> * </ul> * */ public static class Spec { static final Spec UNDEFINED = spec(GridLayout.UNDEFINED); + static final float DEFAULT_WEIGHT = 0; final boolean startDefined; final Interval span; final Alignment alignment; + final float weight; - private Spec(boolean startDefined, Interval span, Alignment alignment) { + private Spec(boolean startDefined, Interval span, Alignment alignment, float weight) { this.startDefined = startDefined; this.span = span; this.alignment = alignment; + this.weight = weight; } - private Spec(boolean startDefined, int start, int size, Alignment alignment) { - this(startDefined, new Interval(start, start + size), alignment); + private Spec(boolean startDefined, int start, int size, Alignment alignment, float weight) { + this(startDefined, new Interval(start, start + size), alignment, weight); } final Spec copyWriteSpan(Interval span) { - return new Spec(startDefined, span, alignment); + return new Spec(startDefined, span, alignment, weight); } final Spec copyWriteAlignment(Alignment alignment) { - return new Spec(startDefined, span, alignment); + return new Spec(startDefined, span, alignment, weight); } final int getFlexibility() { - return (alignment == UNDEFINED_ALIGNMENT) ? INFLEXIBLE : CAN_STRETCH; + return (alignment == UNDEFINED_ALIGNMENT && weight == 0) ? INFLEXIBLE : CAN_STRETCH; } /** @@ -2478,6 +2589,7 @@ public class GridLayout extends ViewGroup { * <ul> * <li> {@code spec.span = [start, start + size]} </li> * <li> {@code spec.alignment = alignment} </li> + * <li> {@code spec.weight = weight} </li> * </ul> * <p> * To leave the start index undefined, use the value {@link #UNDEFINED}. @@ -2485,9 +2597,55 @@ public class GridLayout extends ViewGroup { * @param start the start * @param size the size * @param alignment the alignment + * @param weight the weight + */ + public static Spec spec(int start, int size, Alignment alignment, float weight) { + return new Spec(start != UNDEFINED, start, size, alignment, weight); + } + + /** + * Equivalent to: {@code spec(start, 1, alignment, weight)}. + * + * @param start the start + * @param alignment the alignment + * @param weight the weight + */ + public static Spec spec(int start, Alignment alignment, float weight) { + return spec(start, 1, alignment, weight); + } + + /** + * Equivalent to: {@code spec(start, 1, default_alignment, weight)} - + * where {@code default_alignment} is specified in + * {@link android.widget.GridLayout.LayoutParams}. + * + * @param start the start + * @param size the size + * @param weight the weight + */ + public static Spec spec(int start, int size, float weight) { + return spec(start, size, UNDEFINED_ALIGNMENT, weight); + } + + /** + * Equivalent to: {@code spec(start, 1, weight)}. + * + * @param start the start + * @param weight the weight + */ + public static Spec spec(int start, float weight) { + return spec(start, 1, weight); + } + + /** + * Equivalent to: {@code spec(start, size, alignment, 0f)}. + * + * @param start the start + * @param size the size + * @param alignment the alignment */ public static Spec spec(int start, int size, Alignment alignment) { - return new Spec(start != UNDEFINED, start, size, alignment); + return spec(start, size, alignment, Spec.DEFAULT_WEIGHT); } /** diff --git a/core/java/android/widget/HorizontalScrollView.java b/core/java/android/widget/HorizontalScrollView.java index 25d4f42..0c65c50 100644 --- a/core/java/android/widget/HorizontalScrollView.java +++ b/core/java/android/widget/HorizontalScrollView.java @@ -616,12 +616,14 @@ public class HorizontalScrollView extends FrameLayout { if (canOverscroll) { final int pulledToX = oldX + deltaX; if (pulledToX < 0) { - mEdgeGlowLeft.onPull((float) deltaX / getWidth()); + mEdgeGlowLeft.onPull((float) deltaX / getWidth(), + 1.f - ev.getY(activePointerIndex) / getHeight()); if (!mEdgeGlowRight.isFinished()) { mEdgeGlowRight.onRelease(); } } else if (pulledToX > range) { - mEdgeGlowRight.onPull((float) deltaX / getWidth()); + mEdgeGlowRight.onPull((float) deltaX / getWidth(), + ev.getY(activePointerIndex) / getHeight()); if (!mEdgeGlowLeft.isFinished()) { mEdgeGlowLeft.onRelease(); } diff --git a/core/java/android/widget/ImageView.java b/core/java/android/widget/ImageView.java index eedacb5..572302a 100644 --- a/core/java/android/widget/ImageView.java +++ b/core/java/android/widget/ImageView.java @@ -664,7 +664,7 @@ public class ImageView extends View { InputStream stream = null; try { stream = mContext.getContentResolver().openInputStream(mUri); - d = Drawable.createFromStreamThemed(stream, null, mContext.getTheme()); + d = Drawable.createFromStream(stream, null); } catch (Exception e) { Log.w("ImageView", "Unable to open content: " + mUri, e); } finally { diff --git a/core/java/android/widget/ProgressBar.java b/core/java/android/widget/ProgressBar.java index f7e81b8..b49938c 100644 --- a/core/java/android/widget/ProgressBar.java +++ b/core/java/android/widget/ProgressBar.java @@ -357,9 +357,8 @@ public class ProgressBar extends View { Shader.TileMode.REPEAT, Shader.TileMode.CLAMP); shapeDrawable.getPaint().setShader(bitmapShader); - // Ensure the color filter and tint are propagated. - shapeDrawable.setTint(bitmap.getTint()); - shapeDrawable.setTintMode(bitmap.getTintMode()); + // Ensure the tint and filter are propagated in the correct order. + shapeDrawable.setTint(bitmap.getTint(), bitmap.getTintMode()); shapeDrawable.setColorFilter(bitmap.getColorFilter()); return clip ? new ClipDrawable( @@ -1066,21 +1065,30 @@ public class ProgressBar extends View { protected synchronized void onDraw(Canvas canvas) { super.onDraw(canvas); - Drawable d = mCurrentDrawable; + drawTrack(canvas); + } + + /** + * Draws the progress bar track. + */ + void drawTrack(Canvas canvas) { + final Drawable d = mCurrentDrawable; if (d != null) { // Translate canvas so a indeterminate circular progress bar with padding // rotates properly in its animation - canvas.save(); + final int saveCount = canvas.save(); + if(isLayoutRtl() && mMirrorForRtl) { canvas.translate(getWidth() - mPaddingRight, mPaddingTop); canvas.scale(-1.0f, 1.0f); } else { canvas.translate(mPaddingLeft, mPaddingTop); } - long time = getDrawingTime(); + + final long time = getDrawingTime(); if (mHasAnimation) { mAnimation.getTransformation(time, mTransformation); - float scale = mTransformation.getAlpha(); + final float scale = mTransformation.getAlpha(); try { mInDrawing = true; d.setLevel((int) (scale * MAX_LEVEL)); @@ -1089,8 +1097,10 @@ public class ProgressBar extends View { } postInvalidateOnAnimation(); } + d.draw(canvas); - canvas.restore(); + canvas.restoreToCount(saveCount); + if (mShouldStartAnimationDrawable && d instanceof Animatable) { ((Animatable) d).start(); mShouldStartAnimationDrawable = false; diff --git a/core/java/android/widget/RtlSpacingHelper.java b/core/java/android/widget/RtlSpacingHelper.java new file mode 100644 index 0000000..f6b116f --- /dev/null +++ b/core/java/android/widget/RtlSpacingHelper.java @@ -0,0 +1,91 @@ +/* + * 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; + +/** + * RtlSpacingHelper manages the relationship between left/right and start/end for views + * that need to maintain both absolute and relative settings for a form of spacing similar + * to view padding. + */ +class RtlSpacingHelper { + public static final int UNDEFINED = Integer.MIN_VALUE; + + private int mLeft = 0; + private int mRight = 0; + private int mStart = UNDEFINED; + private int mEnd = UNDEFINED; + private int mExplicitLeft = 0; + private int mExplicitRight = 0; + + private boolean mIsRtl = false; + private boolean mIsRelative = false; + + public int getLeft() { + return mLeft; + } + + public int getRight() { + return mRight; + } + + public int getStart() { + return mIsRtl ? mRight : mLeft; + } + + public int getEnd() { + return mIsRtl ? mLeft : mRight; + } + + public void setRelative(int start, int end) { + mStart = start; + mEnd = end; + mIsRelative = true; + if (mIsRtl) { + if (end != UNDEFINED) mLeft = end; + if (start != UNDEFINED) mRight = start; + } else { + if (start != UNDEFINED) mLeft = start; + if (end != UNDEFINED) mRight = end; + } + } + + public void setAbsolute(int left, int right) { + mIsRelative = false; + if (left != UNDEFINED) mLeft = mExplicitLeft = left; + if (right != UNDEFINED) mRight = mExplicitRight = right; + } + + public void setDirection(boolean isRtl) { + if (isRtl == mIsRtl) { + return; + } + mIsRtl = isRtl; + if (mIsRelative) { + if (isRtl) { + mLeft = mEnd != UNDEFINED ? mEnd : mExplicitLeft; + mRight = mStart != UNDEFINED ? mStart : mExplicitRight; + } else { + mLeft = mStart != UNDEFINED ? mStart : mExplicitLeft; + mRight = mEnd != UNDEFINED ? mEnd : mExplicitRight; + } + } else { + mLeft = mExplicitLeft; + mRight = mExplicitRight; + } + } +} diff --git a/core/java/android/widget/ScrollView.java b/core/java/android/widget/ScrollView.java index 0fa75a6..fd04890 100644 --- a/core/java/android/widget/ScrollView.java +++ b/core/java/android/widget/ScrollView.java @@ -669,12 +669,14 @@ public class ScrollView extends FrameLayout { } else if (canOverscroll) { final int pulledToY = oldY + deltaY; if (pulledToY < 0) { - mEdgeGlowTop.onPull((float) deltaY / getHeight()); + mEdgeGlowTop.onPull((float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowBottom.isFinished()) { mEdgeGlowBottom.onRelease(); } } else if (pulledToY > range) { - mEdgeGlowBottom.onPull((float) deltaY / getHeight()); + mEdgeGlowBottom.onPull((float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) / getWidth()); if (!mEdgeGlowTop.isFinished()) { mEdgeGlowTop.onRelease(); } diff --git a/core/java/android/widget/SuggestionsAdapter.java b/core/java/android/widget/SuggestionsAdapter.java index 0203301..c8917e0 100644 --- a/core/java/android/widget/SuggestionsAdapter.java +++ b/core/java/android/widget/SuggestionsAdapter.java @@ -574,7 +574,7 @@ class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListene throw new FileNotFoundException("Failed to open " + uri); } try { - return Drawable.createFromStreamThemed(stream, null, mContext.getTheme()); + return Drawable.createFromStream(stream, null); } finally { try { stream.close(); diff --git a/core/java/android/widget/Switch.java b/core/java/android/widget/Switch.java index 08af4de..c5c6e64 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -22,9 +22,11 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.graphics.Insets; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.Region.Op; import android.graphics.drawable.Drawable; import android.text.Layout; import android.text.StaticLayout; @@ -85,6 +87,7 @@ public class Switch extends CompoundButton { private int mThumbTextPadding; private int mSwitchMinWidth; private int mSwitchPadding; + private boolean mSplitTrack; private CharSequence mTextOn; private CharSequence mTextOff; @@ -174,13 +177,13 @@ public class Switch extends CompoundButton { super(context, attrs, defStyleAttr, defStyleRes); mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); - Resources res = getResources(); + + final Resources res = getResources(); mTextPaint.density = res.getDisplayMetrics().density; mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale); 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); mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn); @@ -191,15 +194,16 @@ public class Switch extends CompoundButton { com.android.internal.R.styleable.Switch_switchMinWidth, 0); mSwitchPadding = a.getDimensionPixelSize( com.android.internal.R.styleable.Switch_switchPadding, 0); + mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false); - int appearance = a.getResourceId( + final int appearance = a.getResourceId( com.android.internal.R.styleable.Switch_switchTextAppearance, 0); if (appearance != 0) { setSwitchTextAppearance(context, appearance); } a.recycle(); - ViewConfiguration config = ViewConfiguration.get(context); + final ViewConfiguration config = ViewConfiguration.get(context); mTouchSlop = config.getScaledTouchSlop(); mMinFlingVelocity = config.getScaledMinimumFlingVelocity(); @@ -469,6 +473,29 @@ public class Switch extends CompoundButton { } /** + * Specifies whether the track should be split by the thumb. When true, + * the thumb's optical bounds will be clipped out of the track drawable, + * then the thumb will be drawn into the resulting gap. + * + * @param splitTrack Whether the track should be split by the thumb + * + * @attr ref android.R.styleable#Switch_splitTrack + */ + public void setSplitTrack(boolean splitTrack) { + mSplitTrack = splitTrack; + invalidate(); + } + + /** + * Returns whether the track should be split by the thumb. + * + * @attr ref android.R.styleable#Switch_splitTrack + */ + public boolean getSplitTrack() { + return mSplitTrack; + } + + /** * Returns the text displayed when the button is in the checked state. * * @attr ref android.R.styleable#Switch_textOn @@ -518,13 +545,15 @@ public class Switch extends CompoundButton { mTrackDrawable.getPadding(mTempRect); - final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()); + final int maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth()) + + mThumbTextPadding * 2; + mThumbWidth = Math.max(maxTextWidth, mThumbDrawable.getIntrinsicWidth()); + final int switchWidth = Math.max(mSwitchMinWidth, - maxTextWidth * 2 + mThumbTextPadding * 4 + mTempRect.left + mTempRect.right); + 2 * mThumbWidth + mTempRect.left + mTempRect.right); final int switchHeight = Math.max(mTrackDrawable.getIntrinsicHeight(), mThumbDrawable.getIntrinsicHeight()); - mThumbWidth = maxTextWidth + mThumbTextPadding * 2; mSwitchWidth = switchWidth; mSwitchHeight = switchHeight; @@ -637,6 +666,8 @@ public class Switch extends CompoundButton { case MotionEvent.ACTION_CANCEL: { if (mTouchMode == TOUCH_MODE_DRAGGING) { stopDrag(ev); + // Allow super class to handle pressed state, etc. + super.onTouchEvent(ev); return true; } mTouchMode = TOUCH_MODE_IDLE; @@ -772,12 +803,12 @@ public class Switch extends CompoundButton { } @Override - protected void onDraw(Canvas canvas) { + public void draw(Canvas c) { final Rect tempRect = mTempRect; final Drawable trackDrawable = mTrackDrawable; final Drawable thumbDrawable = mThumbDrawable; - // Draw the switch + // Layout the track. final int switchLeft = mSwitchLeft; final int switchTop = mSwitchTop; final int switchRight = mSwitchRight; @@ -786,40 +817,69 @@ public class Switch extends CompoundButton { 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(); + // Layout the thumb. thumbDrawable.getPadding(tempRect); - int thumbLeft = switchInnerLeft - tempRect.left + thumbPos; - int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right; + final int thumbLeft = switchInnerLeft - tempRect.left + thumbPos; + final int thumbRight = switchInnerLeft + thumbPos + mThumbWidth + tempRect.right; thumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom); final Drawable background = getBackground(); - if (background != null && background.supportsHotspots()) { + if (background != null) { background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom); } + // Draw the background. + super.draw(c); + } + + @Override + protected void onDraw(Canvas canvas) { super.onDraw(canvas); - trackDrawable.draw(canvas); + final Rect tempRect = mTempRect; + final Drawable trackDrawable = mTrackDrawable; + final Drawable thumbDrawable = mThumbDrawable; + trackDrawable.getPadding(tempRect); + + final int switchTop = mSwitchTop; + final int switchBottom = mSwitchBottom; + final int switchInnerLeft = mSwitchLeft + tempRect.left; + final int switchInnerTop = switchTop + tempRect.top; + final int switchInnerRight = mSwitchRight - tempRect.right; + final int switchInnerBottom = switchBottom - tempRect.bottom; + + if (mSplitTrack) { + final Insets insets = thumbDrawable.getOpticalInsets(); + thumbDrawable.copyBounds(tempRect); + tempRect.left += insets.left; + tempRect.right -= insets.right; + + final int saveCount = canvas.save(); + canvas.clipRect(tempRect, Op.DIFFERENCE); + trackDrawable.draw(canvas); + canvas.restoreToCount(saveCount); + } else { + trackDrawable.draw(canvas); + } final int saveCount = canvas.save(); canvas.clipRect(switchInnerLeft, switchTop, switchInnerRight, switchBottom); thumbDrawable.draw(canvas); - final int drawableState[] = getDrawableState(); - if (mTextColors != null) { - mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); - } - mTextPaint.drawableState = drawableState; - final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout; if (switchText != null) { - final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2; + final int drawableState[] = getDrawableState(); + if (mTextColors != null) { + mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0)); + } + mTextPaint.drawableState = drawableState; + + final Rect thumbBounds = thumbDrawable.getBounds(); + final int left = (thumbBounds.left + thumbBounds.right) / 2 - switchText.getWidth() / 2; final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2; canvas.translate(left, top); switchText.draw(canvas); @@ -889,17 +949,30 @@ public class Switch extends CompoundButton { protected void drawableStateChanged() { super.drawableStateChanged(); - int[] myDrawableState = getDrawableState(); + final int[] myDrawableState = getDrawableState(); - // Set the state of the Drawable - // Drawable may be null when checked state is set from XML, from super constructor - if (mThumbDrawable != null) mThumbDrawable.setState(myDrawableState); - if (mTrackDrawable != null) mTrackDrawable.setState(myDrawableState); + if (mThumbDrawable != null) { + mThumbDrawable.setState(myDrawableState); + } + + if (mTrackDrawable != null) { + mTrackDrawable.setState(myDrawableState); + } invalidate(); } @Override + public void invalidateDrawable(Drawable drawable) { + super.invalidateDrawable(drawable); + + if (drawable == mThumbDrawable) { + // Handle changes to thumb width and height. + requestLayout(); + } + } + + @Override protected boolean verifyDrawable(Drawable who) { return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable; } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 8f073de..a4a9680 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -8194,7 +8194,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final boolean isPassword = hasPasswordTransformationMethod(); info.setPassword(isPassword); - if (!isPassword) { + if (!isPassword || shouldSpeakPasswordsForAccessibility()) { info.setText(getTextForAccessibility()); } diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java index 075feba..419c582 100644 --- a/core/java/android/widget/Toolbar.java +++ b/core/java/android/widget/Toolbar.java @@ -18,13 +18,17 @@ package android.widget; import android.annotation.NonNull; +import android.app.ActionBar; 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.Layout; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Log; +import android.view.CollapsibleActionView; import android.view.Gravity; import android.view.Menu; import android.view.MenuInflater; @@ -32,7 +36,15 @@ import android.view.MenuItem; import android.view.View; import android.view.ViewDebug; import android.view.ViewGroup; +import android.view.Window; import com.android.internal.R; +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; +import com.android.internal.view.menu.SubMenuBuilder; +import com.android.internal.widget.DecorToolbar; +import com.android.internal.widget.ToolbarWidgetWrapper; import java.util.ArrayList; import java.util.List; @@ -80,19 +92,33 @@ import java.util.List; * layout is discouraged on API 21 devices and newer.</p> */ public class Toolbar extends ViewGroup { + private static final String TAG = "Toolbar"; + private ActionMenuView mMenuView; private TextView mTitleTextView; private TextView mSubtitleTextView; private ImageButton mNavButtonView; private ImageView mLogoView; + private Drawable mCollapseIcon; + private ImageButton mCollapseButtonView; + View mExpandedActionView; + private int mTitleTextAppearance; private int mSubtitleTextAppearance; + private int mNavButtonStyle; + + private int mButtonGravity; + + private int mMaxButtonHeight; + private int mTitleMarginStart; private int mTitleMarginEnd; private int mTitleMarginTop; private int mTitleMarginBottom; + private final RtlSpacingHelper mContentInsets = new RtlSpacingHelper(); + private int mGravity = Gravity.START | Gravity.CENTER_VERTICAL; private CharSequence mTitleText; @@ -101,6 +127,8 @@ public class Toolbar extends ViewGroup { // Clear me after use. private final ArrayList<View> mTempViews = new ArrayList<View>(); + private final int[] mTempMargins = new int[2]; + private OnMenuItemClickListener mOnMenuItemClickListener; private final ActionMenuView.OnMenuItemClickListener mMenuViewItemClickListener = @@ -114,6 +142,10 @@ public class Toolbar extends ViewGroup { } }; + private ToolbarWidgetWrapper mWrapper; + private ActionMenuPresenter mOuterActionMenuPresenter; + private ExpandedActionViewMenuPresenter mExpandedMenuPresenter; + public Toolbar(Context context) { this(context, null); } @@ -134,9 +166,11 @@ public class Toolbar extends ViewGroup { mTitleTextAppearance = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, 0); mSubtitleTextAppearance = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, 0); + mNavButtonStyle = a.getResourceId(R.styleable.Toolbar_navigationButtonStyle, 0); mGravity = a.getInteger(R.styleable.Toolbar_gravity, mGravity); - mTitleMarginStart = mTitleMarginEnd = Math.max(0, a.getDimensionPixelOffset( - R.styleable.Toolbar_titleMargins, -1)); + mButtonGravity = a.getInteger(R.styleable.Toolbar_buttonGravity, Gravity.TOP); + mTitleMarginStart = mTitleMarginEnd = mTitleMarginTop = mTitleMarginBottom = + a.getDimensionPixelOffset(R.styleable.Toolbar_titleMargins, 0); final int marginStart = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginStart, -1); if (marginStart >= 0) { @@ -159,6 +193,28 @@ public class Toolbar extends ViewGroup { mTitleMarginBottom = marginBottom; } + mMaxButtonHeight = a.getDimensionPixelSize(R.styleable.Toolbar_maxButtonHeight, -1); + + final int contentInsetStart = + a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetStart, + RtlSpacingHelper.UNDEFINED); + final int contentInsetEnd = + a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetEnd, + RtlSpacingHelper.UNDEFINED); + final int contentInsetLeft = + a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetLeft, 0); + final int contentInsetRight = + a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetRight, 0); + + mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight); + + if (contentInsetStart != RtlSpacingHelper.UNDEFINED || + contentInsetEnd != RtlSpacingHelper.UNDEFINED) { + mContentInsets.setRelative(contentInsetStart, contentInsetEnd); + } + + mCollapseIcon = a.getDrawable(R.styleable.Toolbar_collapseIcon); + final CharSequence title = a.getText(R.styleable.Toolbar_title); if (!TextUtils.isEmpty(title)) { setTitle(title); @@ -166,11 +222,17 @@ public class Toolbar extends ViewGroup { final CharSequence subtitle = a.getText(R.styleable.Toolbar_subtitle); if (!TextUtils.isEmpty(subtitle)) { - setSubtitle(title); + setSubtitle(subtitle); } a.recycle(); } + @Override + public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) { + super.onRtlPropertiesChanged(layoutDirection); + mContentInsets.setDirection(layoutDirection == LAYOUT_DIRECTION_RTL); + } + /** * Set a logo drawable from a resource id. * @@ -184,6 +246,110 @@ public class Toolbar extends ViewGroup { setLogo(getContext().getDrawable(resId)); } + /** @hide */ + public boolean canShowOverflowMenu() { + return getVisibility() == VISIBLE && mMenuView != null && mMenuView.isOverflowReserved(); + } + + /** + * Check whether the overflow menu is currently showing. This may not reflect + * a pending show operation in progress. + * + * @return true if the overflow menu is currently showing + */ + public boolean isOverflowMenuShowing() { + return mMenuView != null && mMenuView.isOverflowMenuShowing(); + } + + /** @hide */ + public boolean isOverflowMenuShowPending() { + return mMenuView != null && mMenuView.isOverflowMenuShowPending(); + } + + /** + * Show the overflow items from the associated menu. + * + * @return true if the menu was able to be shown, false otherwise + */ + public boolean showOverflowMenu() { + return mMenuView != null && mMenuView.showOverflowMenu(); + } + + /** + * Hide the overflow items from the associated menu. + * + * @return true if the menu was able to be hidden, false otherwise + */ + public boolean hideOverflowMenu() { + return mMenuView != null && mMenuView.hideOverflowMenu(); + } + + /** @hide */ + public void setMenu(MenuBuilder menu, ActionMenuPresenter outerPresenter) { + if (menu == null && mMenuView == null) { + return; + } + + ensureMenuView(); + final MenuBuilder oldMenu = mMenuView.peekMenu(); + if (oldMenu == menu) { + return; + } + + if (oldMenu != null) { + oldMenu.removeMenuPresenter(mOuterActionMenuPresenter); + oldMenu.removeMenuPresenter(mExpandedMenuPresenter); + } + + final Context context = getContext(); + + if (mExpandedMenuPresenter == null) { + mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter(); + } + + outerPresenter.setExpandedActionViewsExclusive(true); + if (menu != null) { + menu.addMenuPresenter(outerPresenter); + menu.addMenuPresenter(mExpandedMenuPresenter); + } else { + outerPresenter.initForMenu(context, null); + mExpandedMenuPresenter.initForMenu(context, null); + outerPresenter.updateMenuView(true); + mExpandedMenuPresenter.updateMenuView(true); + } + mMenuView.setPresenter(outerPresenter); + mOuterActionMenuPresenter = outerPresenter; + } + + /** + * Dismiss all currently showing popup menus, including overflow or submenus. + */ + public void dismissPopupMenus() { + if (mMenuView != null) { + mMenuView.dismissPopupMenus(); + } + } + + /** @hide */ + public boolean isTitleTruncated() { + if (mTitleTextView == null) { + return false; + } + + final Layout titleLayout = mTitleTextView.getLayout(); + if (titleLayout == null) { + return false; + } + + final int lineCount = titleLayout.getLineCount(); + for (int i = 0; i < lineCount; i++) { + if (titleLayout.getEllipsisCount(i) > 0) { + return true; + } + } + return false; + } + /** * Set a logo drawable. * @@ -195,9 +361,7 @@ public class Toolbar extends ViewGroup { */ public void setLogo(Drawable drawable) { if (drawable != null) { - if (mLogoView == null) { - mLogoView = new ImageView(getContext()); - } + ensureLogoView(); if (mLogoView.getParent() == null) { addSystemView(mLogoView); } @@ -241,8 +405,8 @@ public class Toolbar extends ViewGroup { * @param description Description to set */ public void setLogoDescription(CharSequence description) { - if (!TextUtils.isEmpty(description) && mLogoView == null) { - mLogoView = new ImageView(getContext()); + if (!TextUtils.isEmpty(description)) { + ensureLogoView(); } if (mLogoView != null) { mLogoView.setContentDescription(description); @@ -258,10 +422,48 @@ public class Toolbar extends ViewGroup { return mLogoView != null ? mLogoView.getContentDescription() : null; } + private void ensureLogoView() { + if (mLogoView == null) { + mLogoView = new ImageView(getContext()); + } + } + /** - * Return the current title displayed in the toolbar. + * Check whether this Toolbar is currently hosting an expanded action view. + * + * <p>An action view may be expanded either directly from the + * {@link android.view.MenuItem MenuItem} it belongs to or by user action. If the Toolbar + * has an expanded action view it can be collapsed using the {@link #collapseActionView()} + * method.</p> * - * @return The current title + * @return true if the Toolbar has an expanded action view + */ + public boolean hasExpandedActionView() { + return mExpandedMenuPresenter != null && + mExpandedMenuPresenter.mCurrentExpandedItem != null; + } + + /** + * Collapse a currently expanded action view. If this Toolbar does not have an + * expanded action view this method has no effect. + * + * <p>An action view may be expanded either directly from the + * {@link android.view.MenuItem MenuItem} it belongs to or by user action.</p> + * + * @see #hasExpandedActionView() + */ + public void collapseActionView() { + final MenuItemImpl item = mExpandedMenuPresenter == null ? null : + mExpandedMenuPresenter.mCurrentExpandedItem; + if (item != null) { + item.collapseActionView(); + } + } + + /** + * Returns the title of this toolbar. + * + * @return The current title. */ public CharSequence getTitle() { return mTitleText; @@ -292,6 +494,8 @@ public class Toolbar extends ViewGroup { if (mTitleTextView == null) { final Context context = getContext(); mTitleTextView = new TextView(context); + mTitleTextView.setSingleLine(); + mTitleTextView.setEllipsize(TextUtils.TruncateAt.END); mTitleTextView.setTextAppearance(context, mTitleTextAppearance); } if (mTitleTextView.getParent() == null) { @@ -338,6 +542,8 @@ public class Toolbar extends ViewGroup { if (mSubtitleTextView == null) { final Context context = getContext(); mSubtitleTextView = new TextView(context); + mSubtitleTextView.setSingleLine(); + mSubtitleTextView.setEllipsize(TextUtils.TruncateAt.END); mSubtitleTextView.setTextAppearance(context, mSubtitleTextAppearance); } if (mSubtitleTextView.getParent() == null) { @@ -353,6 +559,28 @@ public class Toolbar extends ViewGroup { } /** + * Sets the text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setTitleTextAppearance(Context context, int resId) { + mTitleTextAppearance = resId; + if (mTitleTextView != null) { + mTitleTextView.setTextAppearance(context, resId); + } + } + + /** + * Sets the text color, size, style, hint color, and highlight color + * from the specified TextAppearance resource. + */ + public void setSubtitleTextAppearance(Context context, int resId) { + mSubtitleTextAppearance = resId; + if (mSubtitleTextView != null) { + mSubtitleTextView.setTextAppearance(context, 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 @@ -368,6 +596,30 @@ public class Toolbar extends ViewGroup { } /** + * Set a content description for the navigation button if one is present. The content + * description will be read via screen readers or other accessibility systems to explain + * the action of the navigation button. + * + * @param description Content description to set + */ + public void setNavigationContentDescription(CharSequence description) { + ensureNavButtonView(); + mNavButtonView.setContentDescription(description); + } + + /** + * Set a content description for the navigation button if one is present. The content + * description will be read via screen readers or other accessibility systems to explain + * the action of the navigation button. + * + * @param resId Resource ID of a content description string to set + */ + public void setNavigationContentDescription(int resId) { + ensureNavButtonView(); + mNavButtonView.setContentDescription(resId != 0 ? getContext().getText(resId) : null); + } + + /** * 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 @@ -453,12 +705,32 @@ public class Toolbar extends ViewGroup { * @return The toolbar's Menu */ public Menu getMenu() { + ensureMenu(); + return mMenuView.getMenu(); + } + + private void ensureMenu() { + ensureMenuView(); + if (mMenuView.peekMenu() == null) { + // Initialize a new menu for the first time. + final MenuBuilder menu = (MenuBuilder) mMenuView.getMenu(); + if (mExpandedMenuPresenter == null) { + mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter(); + } + mMenuView.setExpandedActionViewsExclusive(true); + menu.addMenuPresenter(mExpandedMenuPresenter); + } + } + + private void ensureMenuView() { if (mMenuView == null) { mMenuView = new ActionMenuView(getContext()); mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener); + final LayoutParams lp = generateDefaultLayoutParams(); + lp.gravity = Gravity.END | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); + mMenuView.setLayoutParams(lp); addSystemView(mMenuView); } - return mMenuView.getMenu(); } private MenuInflater getMenuInflater() { @@ -489,9 +761,145 @@ public class Toolbar extends ViewGroup { mOnMenuItemClickListener = listener; } + /** + * Set the content insets for this toolbar relative to layout direction. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @param contentInsetStart Content inset for the toolbar starting edge + * @param contentInsetEnd Content inset for the toolbar ending edge + * + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetStart() + * @see #getContentInsetEnd() + * @see #getContentInsetLeft() + * @see #getContentInsetRight() + */ + public void setContentInsetsRelative(int contentInsetStart, int contentInsetEnd) { + mContentInsets.setRelative(contentInsetStart, contentInsetEnd); + } + + /** + * Get the starting content inset for this toolbar. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @return The starting content inset for this toolbar + * + * @see #setContentInsetsRelative(int, int) + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetEnd() + * @see #getContentInsetLeft() + * @see #getContentInsetRight() + */ + public int getContentInsetStart() { + return mContentInsets.getStart(); + } + + /** + * Get the ending content inset for this toolbar. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @return The ending content inset for this toolbar + * + * @see #setContentInsetsRelative(int, int) + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetStart() + * @see #getContentInsetLeft() + * @see #getContentInsetRight() + */ + public int getContentInsetEnd() { + return mContentInsets.getEnd(); + } + + /** + * Set the content insets for this toolbar. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @param contentInsetLeft Content inset for the toolbar's left edge + * @param contentInsetRight Content inset for the toolbar's right edge + * + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetStart() + * @see #getContentInsetEnd() + * @see #getContentInsetLeft() + * @see #getContentInsetRight() + */ + public void setContentInsetsAbsolute(int contentInsetLeft, int contentInsetRight) { + mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight); + } + + /** + * Get the left content inset for this toolbar. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @return The left content inset for this toolbar + * + * @see #setContentInsetsRelative(int, int) + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetStart() + * @see #getContentInsetEnd() + * @see #getContentInsetRight() + */ + public int getContentInsetLeft() { + return mContentInsets.getLeft(); + } + + /** + * Get the right content inset for this toolbar. + * + * <p>The content inset affects the valid area for Toolbar content other than + * the navigation button and menu. Insets define the minimum margin for these components + * and can be used to effectively align Toolbar content along well-known gridlines.</p> + * + * @return The right content inset for this toolbar + * + * @see #setContentInsetsRelative(int, int) + * @see #setContentInsetsAbsolute(int, int) + * @see #getContentInsetStart() + * @see #getContentInsetEnd() + * @see #getContentInsetLeft() + */ + public int getContentInsetRight() { + return mContentInsets.getRight(); + } + private void ensureNavButtonView() { if (mNavButtonView == null) { - mNavButtonView = new ImageButton(getContext(), null, R.attr.borderlessButtonStyle); + mNavButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle); + final LayoutParams lp = generateDefaultLayoutParams(); + lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); + mNavButtonView.setLayoutParams(lp); + } + } + + private void ensureCollapseButtonView() { + if (mCollapseButtonView == null) { + mCollapseButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle); + mCollapseButtonView.setImageDrawable(mCollapseIcon); + final LayoutParams lp = generateDefaultLayoutParams(); + lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); + lp.mViewType = LayoutParams.EXPANDED; + mCollapseButtonView.setLayoutParams(lp); + mCollapseButtonView.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + collapseActionView(); + } + }); } } @@ -514,33 +922,121 @@ public class Toolbar extends ViewGroup { super.onRestoreInstanceState(ss.getSuperState()); } + private void measureChildConstrained(View child, int parentWidthSpec, int widthUsed, + int parentHeightSpec, int heightUsed, int heightConstraint) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + int childWidthSpec = getChildMeasureSpec(parentWidthSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + int childHeightSpec = getChildMeasureSpec(parentHeightSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + + final int childHeightMode = MeasureSpec.getMode(childHeightSpec); + if (childHeightMode != MeasureSpec.EXACTLY && heightConstraint >= 0) { + final int size = childHeightMode != MeasureSpec.UNSPECIFIED ? + Math.min(MeasureSpec.getSize(childHeightSpec), heightConstraint) : + heightConstraint; + childHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY); + } + child.measure(childWidthSpec, childHeightSpec); + } + + /** + * Returns the width + uncollapsed margins + */ + private int measureChildCollapseMargins(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed, int[] collapsingMargins) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int leftDiff = lp.leftMargin - collapsingMargins[0]; + final int rightDiff = lp.rightMargin - collapsingMargins[1]; + final int leftMargin = Math.max(0, leftDiff); + final int rightMargin = Math.max(0, rightDiff); + final int hMargins = leftMargin + rightMargin; + collapsingMargins[0] = Math.max(0, -leftDiff); + collapsingMargins[1] = Math.max(0, -rightDiff); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight + hMargins + widthUsed, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + return child.getMeasuredWidth() + hMargins; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = 0; int height = 0; int childState = 0; + final int[] collapsingMargins = mTempMargins; + final int marginStartIndex; + final int marginEndIndex; + if (isLayoutRtl()) { + marginStartIndex = 1; + marginEndIndex = 0; + } else { + marginStartIndex = 0; + marginEndIndex = 1; + } + // System views measure first. + int navWidth = 0; if (shouldLayout(mNavButtonView)) { - measureChildWithMargins(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0); - width += mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView); + measureChildConstrained(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0, + mMaxButtonHeight); + navWidth = mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView); height = Math.max(height, mNavButtonView.getMeasuredHeight() + getVerticalMargins(mNavButtonView)); childState = combineMeasuredStates(childState, mNavButtonView.getMeasuredState()); } + if (shouldLayout(mCollapseButtonView)) { + measureChildConstrained(mCollapseButtonView, widthMeasureSpec, width, + heightMeasureSpec, 0, mMaxButtonHeight); + navWidth = mCollapseButtonView.getMeasuredWidth() + + getHorizontalMargins(mCollapseButtonView); + height = Math.max(height, mCollapseButtonView.getMeasuredHeight() + + getVerticalMargins(mCollapseButtonView)); + childState = combineMeasuredStates(childState, mCollapseButtonView.getMeasuredState()); + } + + final int contentInsetStart = getContentInsetStart(); + width += Math.max(contentInsetStart, navWidth); + collapsingMargins[marginStartIndex] = Math.max(0, contentInsetStart - navWidth); + + int menuWidth = 0; if (shouldLayout(mMenuView)) { - measureChildWithMargins(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0); - width += mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView); + measureChildConstrained(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0, + mMaxButtonHeight); + menuWidth = mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView); height = Math.max(height, mMenuView.getMeasuredHeight() + getVerticalMargins(mMenuView)); childState = combineMeasuredStates(childState, mMenuView.getMeasuredState()); } + final int contentInsetEnd = getContentInsetEnd(); + width += Math.max(contentInsetEnd, menuWidth); + collapsingMargins[marginEndIndex] = Math.max(0, contentInsetEnd - menuWidth); + + if (shouldLayout(mExpandedActionView)) { + width += measureChildCollapseMargins(mExpandedActionView, widthMeasureSpec, width, + heightMeasureSpec, 0, collapsingMargins); + height = Math.max(height, mExpandedActionView.getMeasuredHeight() + + getVerticalMargins(mExpandedActionView)); + childState = combineMeasuredStates(childState, mExpandedActionView.getMeasuredState()); + } + if (shouldLayout(mLogoView)) { - measureChildWithMargins(mLogoView, widthMeasureSpec, width, heightMeasureSpec, 0); - width += mLogoView.getMeasuredWidth() + getHorizontalMargins(mLogoView); + width += measureChildCollapseMargins(mLogoView, widthMeasureSpec, width, + heightMeasureSpec, 0, collapsingMargins); height = Math.max(height, mLogoView.getMeasuredHeight() + getVerticalMargins(mLogoView)); childState = combineMeasuredStates(childState, mLogoView.getMeasuredState()); @@ -551,17 +1047,18 @@ public class Toolbar extends ViewGroup { final int titleVertMargins = mTitleMarginTop + mTitleMarginBottom; final int titleHorizMargins = mTitleMarginStart + mTitleMarginEnd; if (shouldLayout(mTitleTextView)) { - measureChildWithMargins(mTitleTextView, widthMeasureSpec, width + titleHorizMargins, - heightMeasureSpec, titleVertMargins); + titleWidth = measureChildCollapseMargins(mTitleTextView, widthMeasureSpec, + width + titleHorizMargins, heightMeasureSpec, titleVertMargins, + collapsingMargins); 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)); + titleWidth = Math.max(titleWidth, measureChildCollapseMargins(mSubtitleTextView, + widthMeasureSpec, width + titleHorizMargins, + heightMeasureSpec, titleHeight + titleVertMargins, + collapsingMargins)); titleHeight += mSubtitleTextView.getMeasuredHeight() + getVerticalMargins(mSubtitleTextView); childState = combineMeasuredStates(childState, mSubtitleTextView.getMeasuredState()); @@ -574,13 +1071,13 @@ public class Toolbar extends ViewGroup { 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)) { + if (lp.mViewType != LayoutParams.CUSTOM || !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); + width += measureChildCollapseMargins(child, widthMeasureSpec, width, + heightMeasureSpec, 0, collapsingMargins); height = Math.max(height, child.getMeasuredHeight() + getVerticalMargins(child)); childState = combineMeasuredStates(childState, child.getMeasuredState()); } @@ -611,27 +1108,51 @@ public class Toolbar extends ViewGroup { int left = paddingLeft; int right = width - paddingRight; + final int[] collapsingMargins = mTempMargins; + collapsingMargins[0] = collapsingMargins[1] = 0; + if (shouldLayout(mNavButtonView)) { if (isRtl) { - right = layoutChildRight(mNavButtonView, right); + right = layoutChildRight(mNavButtonView, right, collapsingMargins); } else { - left = layoutChildLeft(mNavButtonView, left); + left = layoutChildLeft(mNavButtonView, left, collapsingMargins); + } + } + + if (shouldLayout(mCollapseButtonView)) { + if (isRtl) { + right = layoutChildRight(mCollapseButtonView, right, collapsingMargins); + } else { + left = layoutChildLeft(mCollapseButtonView, left, collapsingMargins); } } if (shouldLayout(mMenuView)) { if (isRtl) { - left = layoutChildLeft(mMenuView, left); + left = layoutChildLeft(mMenuView, left, collapsingMargins); } else { - right = layoutChildRight(mMenuView, right); + right = layoutChildRight(mMenuView, right, collapsingMargins); + } + } + + collapsingMargins[0] = Math.max(0, getContentInsetLeft() - left); + collapsingMargins[1] = Math.max(0, getContentInsetRight() - (width - paddingRight - right)); + left = Math.max(left, getContentInsetLeft()); + right = Math.min(right, width - paddingRight - getContentInsetRight()); + + if (shouldLayout(mExpandedActionView)) { + if (isRtl) { + right = layoutChildRight(mExpandedActionView, right, collapsingMargins); + } else { + left = layoutChildLeft(mExpandedActionView, left, collapsingMargins); } } if (shouldLayout(mLogoView)) { if (isRtl) { - right = layoutChildRight(mLogoView, right); + right = layoutChildRight(mLogoView, right, collapsingMargins); } else { - left = layoutChildLeft(mLogoView, left); + left = layoutChildLeft(mLogoView, left, collapsingMargins); } } @@ -649,79 +1170,83 @@ public class Toolbar extends ViewGroup { if (layoutTitle || layoutSubtitle) { int titleTop; + final View topChild = layoutTitle ? mTitleTextView : mSubtitleTextView; + final View bottomChild = layoutSubtitle ? mSubtitleTextView : mTitleTextView; + final LayoutParams toplp = (LayoutParams) topChild.getLayoutParams(); + final LayoutParams bottomlp = (LayoutParams) bottomChild.getLayoutParams(); + switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) { case Gravity.TOP: - titleTop = getPaddingTop(); + titleTop = getPaddingTop() + toplp.topMargin + mTitleMarginTop; 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; + if (spaceAbove < toplp.topMargin + mTitleMarginTop) { + spaceAbove = toplp.topMargin + mTitleMarginTop; } else { final int spaceBelow = height - paddingBottom - titleHeight - spaceAbove - paddingTop; - if (spaceBelow < lp.bottomMargin + mTitleMarginBottom) { + if (spaceBelow < toplp.bottomMargin + mTitleMarginBottom) { spaceAbove = Math.max(0, spaceAbove - - (lp.bottomMargin + mTitleMarginBottom - spaceBelow)); + (bottomlp.bottomMargin + mTitleMarginBottom - spaceBelow)); } } titleTop = paddingTop + spaceAbove; break; case Gravity.BOTTOM: - titleTop = height - paddingBottom - titleHeight; + titleTop = height - paddingBottom - bottomlp.bottomMargin - mTitleMarginBottom - + titleHeight; break; } if (isRtl) { + final int rd = mTitleMarginStart - collapsingMargins[1]; + right -= Math.max(0, rd); + collapsingMargins[1] = Math.max(0, -rd); 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; + titleRight = titleLeft - 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; + subtitleRight = subtitleRight - mTitleMarginEnd; titleTop = subtitleBottom + lp.bottomMargin; } right = Math.max(titleRight, subtitleRight); } else { + final int ld = mTitleMarginStart - collapsingMargins[0]; + left += Math.max(0, ld); + collapsingMargins[0] = Math.max(0, -ld); 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; + titleLeft = titleRight + 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; + subtitleLeft = subtitleRight + mTitleMarginEnd; titleTop = subtitleBottom + lp.bottomMargin; } left = Math.max(titleLeft, subtitleLeft); @@ -734,19 +1259,19 @@ public class Toolbar extends ViewGroup { addCustomViewsWithGravity(mTempViews, Gravity.LEFT); final int leftViewsCount = mTempViews.size(); for (int i = 0; i < leftViewsCount; i++) { - left = layoutChildLeft(getChildAt(i), left); + left = layoutChildLeft(mTempViews.get(i), left, collapsingMargins); } addCustomViewsWithGravity(mTempViews, Gravity.RIGHT); final int rightViewsCount = mTempViews.size(); for (int i = 0; i < rightViewsCount; i++) { - right = layoutChildRight(getChildAt(i), right); + right = layoutChildRight(mTempViews.get(i), right, collapsingMargins); } // 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); + addCustomViewsWithGravity(mTempViews, Gravity.CENTER_HORIZONTAL); + final int centerViewsWidth = getViewListMeasuredWidth(mTempViews, collapsingMargins); final int parentCenter = paddingLeft + (width - paddingLeft - paddingRight) / 2; final int halfCenterViewsWidth = centerViewsWidth / 2; int centerLeft = parentCenter - halfCenterViewsWidth; @@ -759,37 +1284,51 @@ public class Toolbar extends ViewGroup { final int centerViewsCount = mTempViews.size(); for (int i = 0; i < centerViewsCount; i++) { - centerLeft = layoutChildLeft(getChildAt(i), centerLeft); + centerLeft = layoutChildLeft(mTempViews.get(i), centerLeft, collapsingMargins); } mTempViews.clear(); } - private int getViewListMeasuredWidth(List<View> views) { + private int getViewListMeasuredWidth(List<View> views, int[] collapsingMargins) { + int collapseLeft = collapsingMargins[0]; + int collapseRight = collapsingMargins[1]; 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; + final int l = lp.leftMargin - collapseLeft; + final int r = lp.rightMargin - collapseRight; + final int leftMargin = Math.max(0, l); + final int rightMargin = Math.max(0, r); + collapseLeft = Math.max(0, -l); + collapseRight = Math.max(0, -r); + width += leftMargin + v.getMeasuredWidth() + rightMargin; } return width; } - private int layoutChildLeft(View child, int left) { + private int layoutChildLeft(View child, int left, int[] collapsingMargins) { 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; + final int l = lp.leftMargin - collapsingMargins[0]; + left += Math.max(0, l); + collapsingMargins[0] = Math.max(0, -l); + final int top = getChildTop(child); + final int childWidth = child.getMeasuredWidth(); + child.layout(left, top, left + childWidth, top + child.getMeasuredHeight()); + left += childWidth + lp.rightMargin; return left; } - private int layoutChildRight(View child, int right) { + private int layoutChildRight(View child, int right, int[] collapsingMargins) { 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; + final int r = lp.rightMargin - collapsingMargins[1]; + right -= Math.max(0, r); + collapsingMargins[1] = Math.max(0, -r); + final int top = getChildTop(child); + final int childWidth = child.getMeasuredWidth(); + child.layout(right - childWidth, top, right, top + child.getMeasuredHeight()); + right -= childWidth + lp.leftMargin; return right; } @@ -853,17 +1392,16 @@ public class Toolbar extends ViewGroup { 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) && + if (lp.mViewType == LayoutParams.CUSTOM && 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) && + if (lp.mViewType == LayoutParams.CUSTOM && shouldLayout(child) && getChildHorizontalGravity(lp.gravity) == absGrav) { views.add(child); } @@ -900,14 +1438,16 @@ public class Toolbar extends ViewGroup { } @Override - public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { - return super.generateLayoutParams(attrs); + public LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); } @Override - protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { if (p instanceof LayoutParams) { return new LayoutParams((LayoutParams) p); + } else if (p instanceof ActionBar.LayoutParams) { + return new LayoutParams((ActionBar.LayoutParams) p); } else if (p instanceof MarginLayoutParams) { return new LayoutParams((MarginLayoutParams) p); } else { @@ -916,7 +1456,7 @@ public class Toolbar extends ViewGroup { } @Override - protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @@ -929,6 +1469,25 @@ public class Toolbar extends ViewGroup { return ((LayoutParams) child.getLayoutParams()).mViewType == LayoutParams.CUSTOM; } + /** @hide */ + public DecorToolbar getWrapper() { + if (mWrapper == null) { + mWrapper = new ToolbarWidgetWrapper(this); + } + return mWrapper; + } + + private void setChildVisibilityForExpandedActionView(boolean expand) { + 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.EXPANDED && child != mMenuView) { + child.setVisibility(expand ? GONE : VISIBLE); + } + } + } + /** * Interface responsible for receiving menu item click events if the items themselves * do not have individual item click listeners. @@ -949,44 +1508,15 @@ public class Toolbar extends ViewGroup { * * @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; - + public static class LayoutParams extends ActionBar.LayoutParams { static final int CUSTOM = 0; static final int SYSTEM = 1; + static final int EXPANDED = 2; 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) { @@ -1006,7 +1536,11 @@ public class Toolbar extends ViewGroup { public LayoutParams(LayoutParams source) { super(source); - this.gravity = source.gravity; + mViewType = source.mViewType; + } + + public LayoutParams(ActionBar.LayoutParams source) { + super(source); } public LayoutParams(MarginLayoutParams source) { @@ -1045,4 +1579,126 @@ public class Toolbar extends ViewGroup { } }; } + + private class ExpandedActionViewMenuPresenter implements MenuPresenter { + MenuBuilder mMenu; + MenuItemImpl mCurrentExpandedItem; + + @Override + public void initForMenu(Context context, MenuBuilder menu) { + // Clear the expanded action view when menus change. + if (mMenu != null && mCurrentExpandedItem != null) { + mMenu.collapseItemActionView(mCurrentExpandedItem); + } + mMenu = menu; + } + + @Override + public MenuView getMenuView(ViewGroup root) { + return null; + } + + @Override + public void updateMenuView(boolean cleared) { + // Make sure the expanded item we have is still there. + if (mCurrentExpandedItem != null) { + boolean found = false; + + if (mMenu != null) { + final int count = mMenu.size(); + for (int i = 0; i < count; i++) { + final MenuItem item = mMenu.getItem(i); + if (item == mCurrentExpandedItem) { + found = true; + break; + } + } + } + + if (!found) { + // The item we had expanded disappeared. Collapse. + collapseItemActionView(mMenu, mCurrentExpandedItem); + } + } + } + + @Override + public void setCallback(Callback cb) { + } + + @Override + public boolean onSubMenuSelected(SubMenuBuilder subMenu) { + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + } + + @Override + public boolean flagActionItems() { + return false; + } + + @Override + public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) { + ensureCollapseButtonView(); + if (mCollapseButtonView.getParent() != Toolbar.this) { + addView(mCollapseButtonView); + } + mExpandedActionView = item.getActionView(); + mCurrentExpandedItem = item; + if (mExpandedActionView.getParent() != Toolbar.this) { + final LayoutParams lp = generateDefaultLayoutParams(); + lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK); + lp.mViewType = LayoutParams.EXPANDED; + mExpandedActionView.setLayoutParams(lp); + addView(mExpandedActionView); + } + + setChildVisibilityForExpandedActionView(true); + requestLayout(); + item.setActionViewExpanded(true); + + if (mExpandedActionView instanceof CollapsibleActionView) { + ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded(); + } + + return true; + } + + @Override + public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) { + // Do this before detaching the actionview from the hierarchy, in case + // it needs to dismiss the soft keyboard, etc. + if (mExpandedActionView instanceof CollapsibleActionView) { + ((CollapsibleActionView) mExpandedActionView).onActionViewCollapsed(); + } + + removeView(mExpandedActionView); + removeView(mCollapseButtonView); + mExpandedActionView = null; + + setChildVisibilityForExpandedActionView(false); + mCurrentExpandedItem = null; + requestLayout(); + item.setActionViewExpanded(false); + + return true; + } + + @Override + public int getId() { + return 0; + } + + @Override + public Parcelable onSaveInstanceState() { + return null; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + } + } } diff --git a/core/java/android/widget/VideoView.java b/core/java/android/widget/VideoView.java index f23c64f..2b62552 100644 --- a/core/java/android/widget/VideoView.java +++ b/core/java/android/widget/VideoView.java @@ -30,6 +30,7 @@ import android.media.MediaPlayer.OnInfoListener; import android.media.Metadata; import android.media.SubtitleController; import android.media.SubtitleTrack.RenderingWidget; +import android.media.TtmlRenderer; import android.media.WebVttRenderer; import android.net.Uri; import android.os.Looper; @@ -314,6 +315,7 @@ public class VideoView extends SurfaceView final SubtitleController controller = new SubtitleController( context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer); controller.registerRenderer(new WebVttRenderer(context)); + controller.registerRenderer(new TtmlRenderer(context)); mMediaPlayer.setSubtitleAnchor(controller, this); if (mAudioSession != 0) { diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index 4726da7..5547a10 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -26,6 +26,7 @@ import android.content.DialogInterface; import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Handler; import android.os.Message; import android.text.TextUtils; @@ -38,6 +39,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.view.Window; +import android.view.WindowInsets; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -121,11 +123,14 @@ public class AlertController { private int mCheckedItem = -1; private int mAlertDialogLayout; + private int mButtonPanelSideLayout; private int mListLayout; private int mMultiChoiceItemLayout; private int mSingleChoiceItemLayout; private int mListItemLayout; + private int mButtonPanelLayoutHint = AlertDialog.LAYOUT_HINT_NONE; + private Handler mHandler; private final View.OnClickListener mButtonHandler = new View.OnClickListener() { @@ -197,6 +202,9 @@ public class AlertController { mAlertDialogLayout = a.getResourceId(com.android.internal.R.styleable.AlertDialog_layout, com.android.internal.R.layout.alert_dialog); + mButtonPanelSideLayout = a.getResourceId( + com.android.internal.R.styleable.AlertDialog_buttonPanelSideLayout, 0); + mListLayout = a.getResourceId( com.android.internal.R.styleable.AlertDialog_listLayout, com.android.internal.R.layout.select_dialog); @@ -238,8 +246,21 @@ public class AlertController { public void installContent() { /* We use a custom title so never request a window title */ mWindow.requestFeature(Window.FEATURE_NO_TITLE); - mWindow.setContentView(mAlertDialogLayout); + int contentView = selectContentView(); + mWindow.setContentView(contentView); setupView(); + setupDecor(); + } + + private int selectContentView() { + if (mButtonPanelSideLayout == 0) { + return mAlertDialogLayout; + } + if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) { + return mButtonPanelSideLayout; + } + // TODO: use layout hint side for long messages/lists + return mAlertDialogLayout; } public void setTitle(CharSequence title) { @@ -296,6 +317,13 @@ public class AlertController { } /** + * Sets a hint for the best button panel layout. + */ + public void setButtonPanelLayoutHint(int layoutHint) { + mButtonPanelLayoutHint = layoutHint; + } + + /** * Sets a click listener or a message to be sent when the button is clicked. * You only need to pass one of {@code listener} or {@code msg}. * @@ -415,7 +443,28 @@ public class AlertController { public boolean onKeyUp(int keyCode, KeyEvent event) { return mScrollView != null && mScrollView.executeKeyEvent(event); } - + + private void setupDecor() { + final View decor = mWindow.getDecorView(); + final View parent = mWindow.findViewById(R.id.parentPanel); + if (parent != null && decor != null) { + decor.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) { + if (insets.isRound()) { + // TODO: Get the padding as a function of the window size. + int roundOffset = mContext.getResources().getDimensionPixelOffset( + R.dimen.alert_dialog_round_padding); + parent.setPadding(roundOffset, roundOffset, roundOffset, roundOffset); + } + return insets.consumeSystemWindowInsets(); + } + }); + decor.setFitsSystemWindows(true); + decor.requestApplyInsets(); + } + } + private void setupView() { LinearLayout contentPanel = (LinearLayout) mWindow.findViewById(R.id.contentPanel); setupContent(contentPanel); @@ -636,14 +685,31 @@ public class AlertController { 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); - 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); - final int centerDark = a.getResourceId( - R.styleable.AlertDialog_centerDark, R.drawable.popup_center_dark); + int fullDark = 0; + int topDark = 0; + int centerDark = 0; + int bottomDark = 0; + int fullBright = 0; + int topBright = 0; + int centerBright = 0; + int bottomBright = 0; + int bottomMedium = 0; + if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.KITKAT) { + fullDark = R.drawable.popup_full_dark; + topDark = R.drawable.popup_top_dark; + centerDark = R.drawable.popup_center_dark; + bottomDark = R.drawable.popup_bottom_dark; + fullBright = R.drawable.popup_full_bright; + topBright = R.drawable.popup_top_bright; + centerBright = R.drawable.popup_center_bright; + bottomBright = R.drawable.popup_bottom_bright; + bottomMedium = R.drawable.popup_bottom_medium; + } + topBright = a.getResourceId(R.styleable.AlertDialog_topBright, topBright); + topDark = a.getResourceId(R.styleable.AlertDialog_topDark, topDark); + centerBright = a.getResourceId(R.styleable.AlertDialog_centerBright, centerBright); + centerDark = a.getResourceId(R.styleable.AlertDialog_centerDark, centerDark); + /* We now set the background of all of the sections of the alert. * First collect together each section that is being displayed along @@ -707,22 +773,17 @@ public class AlertController { if (lastView != null) { if (setView) { - 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); + bottomBright = a.getResourceId(R.styleable.AlertDialog_bottomBright, bottomBright); + bottomMedium = a.getResourceId(R.styleable.AlertDialog_bottomMedium, bottomMedium); + bottomDark = a.getResourceId(R.styleable.AlertDialog_bottomDark, bottomDark); // 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); + fullBright = a.getResourceId(R.styleable.AlertDialog_fullBright, fullBright); + fullDark = a.getResourceId(R.styleable.AlertDialog_fullDark, fullDark); lastView.setBackgroundResource(lastLight ? fullBright : fullDark); } @@ -965,7 +1026,7 @@ public class AlertController { ? dialog.mSingleChoiceItemLayout : dialog.mListItemLayout; if (mCursor == null) { adapter = (mAdapter != null) ? mAdapter - : new ArrayAdapter<CharSequence>(mContext, layout, R.id.text1, mItems); + : new CheckedItemAdapter(mContext, layout, R.id.text1, mItems); } else { adapter = new SimpleCursorAdapter(mContext, layout, mCursor, new String[]{mLabelColumn}, new int[]{R.id.text1}); @@ -1020,4 +1081,20 @@ public class AlertController { } } + private static class CheckedItemAdapter extends ArrayAdapter<CharSequence> { + public CheckedItemAdapter(Context context, int resource, int textViewResourceId, + CharSequence[] objects) { + super(context, resource, textViewResourceId, objects); + } + + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + return position; + } + } } diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index cd75010..8e6fa58 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -17,6 +17,7 @@ package com.android.internal.app; import android.app.AppOpsManager; +import android.os.Bundle; import com.android.internal.app.IAppOpsCallback; interface IAppOpsService { @@ -38,4 +39,10 @@ interface IAppOpsService { 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); + + void setDeviceOwner(String packageName); + void setProfileOwner(String packageName, int userHandle); + void setUserRestrictions(in Bundle restrictions, int userHandle); + void removeUser(int userHandle); + } diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 04547495..0769b08 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -46,14 +46,15 @@ interface IBatteryStats { 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 noteStopWakelock(int uid, int pid, String name, String historyName, 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, + void noteChangeWakelockFromSource(in WorkSource ws, int pid, String name, String histyoryName, + 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 noteStopWakelockFromSource(in WorkSource ws, int pid, String name, String historyName, + int type); void noteVibratorOn(int uid, long durationMillis); void noteVibratorOff(int uid); diff --git a/core/java/com/android/internal/app/IMediaContainerService.aidl b/core/java/com/android/internal/app/IMediaContainerService.aidl index 03d3b22..77f0dec 100644 --- a/core/java/com/android/internal/app/IMediaContainerService.aidl +++ b/core/java/com/android/internal/app/IMediaContainerService.aidl @@ -25,16 +25,18 @@ import android.content.res.ObbInfo; interface IMediaContainerService { String copyResourceToContainer(in Uri packageURI, String containerId, String key, String resFileName, String publicResFileName, boolean isExternal, - boolean isForwardLocked); + boolean isForwardLocked, in String abiOverride); int copyResource(in Uri packageURI, in ContainerEncryptionParams encryptionParams, in ParcelFileDescriptor outStream); - PackageInfoLite getMinimalPackageInfo(in String packagePath, in int flags, in long threshold); + PackageInfoLite getMinimalPackageInfo(in String packagePath, in int flags, in long threshold, + in String abiOverride); boolean checkInternalFreeStorage(in Uri fileUri, boolean isForwardLocked, in long threshold); - boolean checkExternalFreeStorage(in Uri fileUri, boolean isForwardLocked); + boolean checkExternalFreeStorage(in Uri fileUri, boolean isForwardLocked, in String abiOverride); ObbInfo getObbInfo(in String filename); long calculateDirectorySize(in String directory); /** Return file system stats: [0] is total bytes, [1] is available bytes */ long[] getFileSystemStats(in String path); void clearDirectory(in String directory); - long calculateInstalledSize(in String packagePath, boolean isForwardLocked); + long calculateInstalledSize(in String packagePath, boolean isForwardLocked, + in String abiOverride); } diff --git a/core/java/com/android/internal/app/IVoiceInteractor.aidl b/core/java/com/android/internal/app/IVoiceInteractor.aidl index 737906a..2900595 100644 --- a/core/java/com/android/internal/app/IVoiceInteractor.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractor.aidl @@ -26,7 +26,9 @@ import com.android.internal.app.IVoiceInteractorRequest; */ interface IVoiceInteractor { IVoiceInteractorRequest startConfirmation(String callingPackage, - IVoiceInteractorCallback callback, String prompt, in Bundle extras); + IVoiceInteractorCallback callback, CharSequence prompt, in Bundle extras); + IVoiceInteractorRequest startAbortVoice(String callingPackage, + IVoiceInteractorCallback callback, CharSequence message, 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 index c6f93e1..8dbf9d4 100644 --- a/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractorCallback.aidl @@ -26,6 +26,7 @@ import com.android.internal.app.IVoiceInteractorRequest; oneway interface IVoiceInteractorCallback { void deliverConfirmationResult(IVoiceInteractorRequest request, boolean confirmed, in Bundle result); + void deliverAbortVoiceResult(IVoiceInteractorRequest request, 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/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index 2f74372..01e5d40 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -35,8 +35,8 @@ 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. + * This is used in conjunction with the {@link setCrossProfileIntentFilter} method of + * {@link DevicePolicyManager} to enable intents to be passed in and out of a managed profile. */ public class IntentForwarderActivity extends Activity { @@ -84,6 +84,7 @@ public class IntentForwarderActivity extends Activity { Slog.e(TAG, "PackageManagerService is dead?"); } if (canForward) { + newIntent.prepareToLeaveUser(callingUserId); startActivityAsUser(newIntent, userDest); } else { Slog.wtf(TAG, "the intent: " + newIntent + "cannot be forwarded from user " diff --git a/core/java/com/android/internal/app/ProcessStats.java b/core/java/com/android/internal/app/ProcessStats.java index 882bec9..7e11850 100644 --- a/core/java/com/android/internal/app/ProcessStats.java +++ b/core/java/com/android/internal/app/ProcessStats.java @@ -1100,7 +1100,7 @@ public final class ProcessStats implements Parcelable { public boolean evaluateSystemProperties(boolean update) { boolean changed = false; - String runtime = SystemProperties.get("persist.sys.dalvik.vm.lib.1", + String runtime = SystemProperties.get("persist.sys.dalvik.vm.lib.2", VMRuntime.getRuntime().vmLibrary()); if (!Objects.equals(runtime, mRuntime)) { changed = true; @@ -1108,13 +1108,6 @@ public final class ProcessStats implements Parcelable { mRuntime = runtime; } } - String webview = WebViewFactory.useExperimentalWebView() ? "chromeview" : "webview"; - if (!Objects.equals(webview, mWebView)) { - changed = true; - if (update) { - mWebView = webview; - } - } return changed; } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 591267e..183dd05 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -484,8 +484,7 @@ public class ResolverActivity extends AlertActivity implements AdapterView.OnIte mList.clear(); if (mBaseResolveList != null) { - currentResolveList = mBaseResolveList; - mOrigResolveList = null; + currentResolveList = mOrigResolveList = mBaseResolveList; } else { currentResolveList = mOrigResolveList = mPm.queryIntentActivities( mIntent, PackageManager.MATCH_DEFAULT_ONLY diff --git a/core/java/com/android/internal/app/ToolbarActionBar.java b/core/java/com/android/internal/app/ToolbarActionBar.java index afb6f7c..6056bf2 100644 --- a/core/java/com/android/internal/app/ToolbarActionBar.java +++ b/core/java/com/android/internal/app/ToolbarActionBar.java @@ -23,37 +23,50 @@ import android.content.Context; import android.content.res.Configuration; import android.graphics.drawable.Drawable; import android.view.ActionMode; +import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.view.Window; import android.widget.SpinnerAdapter; import android.widget.Toolbar; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.widget.DecorToolbar; +import com.android.internal.widget.ToolbarWidgetWrapper; 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 DecorToolbar mDecorToolbar; + private Window.Callback mWindowCallback; private boolean mLastMenuVisibility; private ArrayList<OnMenuVisibilityListener> mMenuVisibilityListeners = new ArrayList<OnMenuVisibilityListener>(); - public ToolbarActionBar(Toolbar toolbar) { + private final Runnable mMenuInvalidator = new Runnable() { + @Override + public void run() { + populateOptionsMenu(); + } + }; + + private final Toolbar.OnMenuItemClickListener mMenuClicker = + new Toolbar.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + return mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, item); + } + }; + + public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback windowCallback) { mToolbar = toolbar; + mDecorToolbar = new ToolbarWidgetWrapper(toolbar); + mWindowCallback = windowCallback; + toolbar.setOnMenuItemClickListener(mMenuClicker); + mDecorToolbar.setWindowTitle(title); } @Override @@ -63,19 +76,8 @@ public class ToolbarActionBar extends ActionBar { @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; + view.setLayoutParams(layoutParams); + mDecorToolbar.setCustomView(view); } @Override @@ -86,48 +88,22 @@ public class ToolbarActionBar extends ActionBar { @Override public void setIcon(int resId) { - mIconResId = resId; - mIconDrawable = null; - updateToolbarLogo(); + mDecorToolbar.setIcon(resId); } @Override public void setIcon(Drawable icon) { - mIconResId = 0; - mIconDrawable = icon; - updateToolbarLogo(); + mDecorToolbar.setIcon(icon); } @Override public void setLogo(int resId) { - mLogoResId = resId; - mLogoDrawable = null; - updateToolbarLogo(); + mDecorToolbar.setLogo(resId); } @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); + mDecorToolbar.setLogo(logo); } @Override @@ -219,42 +195,22 @@ public class ToolbarActionBar extends ActionBar { @Override public void setTitle(CharSequence title) { - mTitle = title; - mTitleResId = 0; - updateToolbarTitle(); + mDecorToolbar.setTitle(title); } @Override public void setTitle(int resId) { - mTitleResId = resId; - mTitle = null; - updateToolbarTitle(); + mDecorToolbar.setTitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null); } @Override public void setSubtitle(CharSequence subtitle) { - mSubtitle = subtitle; - mSubtitleResId = 0; - updateToolbarTitle(); + mDecorToolbar.setSubtitle(subtitle); } @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); + mDecorToolbar.setSubtitle(resId != 0 ? mDecorToolbar.getContext().getText(resId) : null); } @Override @@ -264,9 +220,8 @@ public class ToolbarActionBar extends ActionBar { @Override public void setDisplayOptions(@DisplayOptions int options, @DisplayOptions int mask) { - final int oldOptions = mDisplayOptions; - mDisplayOptions = (options & mask) | (mDisplayOptions & ~mask); - final int optionsChanged = oldOptions ^ mDisplayOptions; + mDecorToolbar.setDisplayOptions((options & mask) | + mDecorToolbar.getDisplayOptions() & ~mask); } @Override @@ -301,7 +256,7 @@ public class ToolbarActionBar extends ActionBar { @Override public View getCustomView() { - return mCustomView; + return mDecorToolbar.getCustomView(); } @Override @@ -327,7 +282,7 @@ public class ToolbarActionBar extends ActionBar { @Override public int getDisplayOptions() { - return mDisplayOptions; + return mDecorToolbar.getDisplayOptions(); } @Override @@ -425,6 +380,54 @@ public class ToolbarActionBar extends ActionBar { return mToolbar.getVisibility() == View.VISIBLE; } + @Override + public boolean openOptionsMenu() { + return mToolbar.showOverflowMenu(); + } + + @Override + public boolean invalidateOptionsMenu() { + mToolbar.removeCallbacks(mMenuInvalidator); + mToolbar.postOnAnimation(mMenuInvalidator); + return true; + } + + @Override + public boolean collapseActionView() { + if (mToolbar.hasExpandedActionView()) { + mToolbar.collapseActionView(); + return true; + } + return false; + } + + void populateOptionsMenu() { + final Menu menu = mToolbar.getMenu(); + final MenuBuilder mb = menu instanceof MenuBuilder ? (MenuBuilder) menu : null; + if (mb != null) { + mb.stopDispatchingItemsChanged(); + } + try { + menu.clear(); + if (!mWindowCallback.onCreatePanelMenu(Window.FEATURE_OPTIONS_PANEL, menu) || + !mWindowCallback.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, null, menu)) { + menu.clear(); + } + } finally { + if (mb != null) { + mb.startDispatchingItemsChanged(); + } + } + } + + @Override + public boolean onMenuKeyEvent(KeyEvent event) { + if (event.getAction() == KeyEvent.ACTION_UP) { + openOptionsMenu(); + } + return true; + } + public void addOnMenuVisibilityListener(OnMenuVisibilityListener listener) { mMenuVisibilityListeners.add(listener); } diff --git a/core/java/com/android/internal/app/WindowDecorActionBar.java b/core/java/com/android/internal/app/WindowDecorActionBar.java index 66548f0..c0b5b97 100644 --- a/core/java/com/android/internal/app/WindowDecorActionBar.java +++ b/core/java/com/android/internal/app/WindowDecorActionBar.java @@ -18,7 +18,10 @@ package com.android.internal.app; import android.animation.ValueAnimator; import android.content.res.TypedArray; +import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.AdapterView; +import android.widget.Toolbar; import com.android.internal.R; import com.android.internal.view.ActionBarPolicy; import com.android.internal.view.menu.MenuBuilder; @@ -28,6 +31,7 @@ import com.android.internal.widget.ActionBarContainer; import com.android.internal.widget.ActionBarContextView; import com.android.internal.widget.ActionBarOverlayLayout; import com.android.internal.widget.ActionBarView; +import com.android.internal.widget.DecorToolbar; import com.android.internal.widget.ScrollingTabContainerView; import android.animation.Animator; @@ -55,6 +59,7 @@ import android.view.Window; import android.view.accessibility.AccessibilityEvent; import android.view.animation.AnimationUtils; import android.widget.SpinnerAdapter; +import com.android.internal.widget.ToolbarWidgetWrapper; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -77,7 +82,7 @@ public class WindowDecorActionBar extends ActionBar implements private ActionBarOverlayLayout mOverlayLayout; private ActionBarContainer mContainerView; - private ActionBarView mActionView; + private DecorToolbar mDecorToolbar; private ActionBarContextView mContextView; private ActionBarContainer mSplitView; private View mContentView; @@ -183,11 +188,11 @@ public class WindowDecorActionBar extends ActionBar implements private void init(View decor) { mOverlayLayout = (ActionBarOverlayLayout) decor.findViewById( - com.android.internal.R.id.action_bar_overlay_layout); + com.android.internal.R.id.decor_content_parent); if (mOverlayLayout != null) { mOverlayLayout.setActionBarVisibilityCallback(this); } - mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar); + mDecorToolbar = getDecorToolbar(decor.findViewById(com.android.internal.R.id.action_bar)); mContextView = (ActionBarContextView) decor.findViewById( com.android.internal.R.id.action_context_bar); mContainerView = (ActionBarContainer) decor.findViewById( @@ -195,18 +200,17 @@ public class WindowDecorActionBar extends ActionBar implements mSplitView = (ActionBarContainer) decor.findViewById( com.android.internal.R.id.split_action_bar); - if (mActionView == null || mContextView == null || mContainerView == null) { + if (mDecorToolbar == null || mContextView == null || mContainerView == null) { throw new IllegalStateException(getClass().getSimpleName() + " can only be used " + "with a compatible window decor layout"); } - mContext = mActionView.getContext(); - mActionView.setContextView(mContextView); - mContextDisplayMode = mActionView.isSplitActionBar() ? + mContext = mDecorToolbar.getContext(); + mContextDisplayMode = mDecorToolbar.isSplit() ? CONTEXT_DISPLAY_SPLIT : CONTEXT_DISPLAY_NORMAL; // This was initially read from the action bar style - final int current = mActionView.getDisplayOptions(); + final int current = mDecorToolbar.getDisplayOptions(); final boolean homeAsUp = (current & DISPLAY_HOME_AS_UP) != 0; if (homeAsUp) { mDisplayHomeAsUpSet = true; @@ -225,6 +229,17 @@ public class WindowDecorActionBar extends ActionBar implements a.recycle(); } + private DecorToolbar getDecorToolbar(View view) { + if (view instanceof DecorToolbar) { + return (DecorToolbar) view; + } else if (view instanceof Toolbar) { + return ((Toolbar) view).getWrapper(); + } else { + throw new IllegalStateException("Can't make a decor toolbar out of " + + view.getClass().getSimpleName()); + } + } + public void onConfigurationChanged(Configuration newConfig) { setHasEmbeddedTabs(ActionBarPolicy.get(mContext).hasEmbeddedTabs()); } @@ -233,11 +248,11 @@ public class WindowDecorActionBar extends ActionBar implements mHasEmbeddedTabs = hasEmbeddedTabs; // Switch tab layout configuration if needed if (!mHasEmbeddedTabs) { - mActionView.setEmbeddedTabView(null); + mDecorToolbar.setEmbeddedTabView(null); mContainerView.setTabContainer(mTabScrollView); } else { mContainerView.setTabContainer(null); - mActionView.setEmbeddedTabView(mTabScrollView); + mDecorToolbar.setEmbeddedTabView(mTabScrollView); } final boolean isInTabMode = getNavigationMode() == NAVIGATION_MODE_TABS; if (mTabScrollView != null) { @@ -250,7 +265,7 @@ public class WindowDecorActionBar extends ActionBar implements mTabScrollView.setVisibility(View.GONE); } } - mActionView.setCollapsable(!mHasEmbeddedTabs && isInTabMode); + mDecorToolbar.setCollapsible(!mHasEmbeddedTabs && isInTabMode); mOverlayLayout.setHasNonEmbeddedTabs(!mHasEmbeddedTabs && isInTabMode); } @@ -263,12 +278,12 @@ public class WindowDecorActionBar extends ActionBar implements if (mHasEmbeddedTabs) { tabScroller.setVisibility(View.VISIBLE); - mActionView.setEmbeddedTabView(tabScroller); + mDecorToolbar.setEmbeddedTabView(tabScroller); } else { if (getNavigationMode() == NAVIGATION_MODE_TABS) { tabScroller.setVisibility(View.VISIBLE); if (mOverlayLayout != null) { - mOverlayLayout.requestFitSystemWindows(); + mOverlayLayout.requestApplyInsets(); } } else { tabScroller.setVisibility(View.GONE); @@ -326,7 +341,8 @@ public class WindowDecorActionBar extends ActionBar implements @Override public void setCustomView(int resId) { - setCustomView(LayoutInflater.from(getThemedContext()).inflate(resId, mActionView, false)); + setCustomView(LayoutInflater.from(getThemedContext()).inflate(resId, + mDecorToolbar.getViewGroup(), false)); } @Override @@ -356,7 +372,7 @@ public class WindowDecorActionBar extends ActionBar implements @Override public void setHomeButtonEnabled(boolean enable) { - mActionView.setHomeButtonEnabled(enable); + mDecorToolbar.setHomeButtonEnabled(enable); } @Override @@ -370,12 +386,12 @@ public class WindowDecorActionBar extends ActionBar implements } public void setSelectedNavigationItem(int position) { - switch (mActionView.getNavigationMode()) { + switch (mDecorToolbar.getNavigationMode()) { case NAVIGATION_MODE_TABS: selectTab(mTabs.get(position)); break; case NAVIGATION_MODE_LIST: - mActionView.setDropdownSelectedPosition(position); + mDecorToolbar.setDropdownSelectedPosition(position); break; default: throw new IllegalStateException( @@ -399,26 +415,26 @@ public class WindowDecorActionBar extends ActionBar implements } public void setTitle(CharSequence title) { - mActionView.setTitle(title); + mDecorToolbar.setTitle(title); } public void setSubtitle(CharSequence subtitle) { - mActionView.setSubtitle(subtitle); + mDecorToolbar.setSubtitle(subtitle); } public void setDisplayOptions(int options) { if ((options & DISPLAY_HOME_AS_UP) != 0) { mDisplayHomeAsUpSet = true; } - mActionView.setDisplayOptions(options); + mDecorToolbar.setDisplayOptions(options); } public void setDisplayOptions(int options, int mask) { - final int current = mActionView.getDisplayOptions(); + final int current = mDecorToolbar.getDisplayOptions(); if ((mask & DISPLAY_HOME_AS_UP) != 0) { mDisplayHomeAsUpSet = true; } - mActionView.setDisplayOptions((options & mask) | (current & ~mask)); + mDecorToolbar.setDisplayOptions((options & mask) | (current & ~mask)); } public void setBackgroundDrawable(Drawable d) { @@ -436,23 +452,23 @@ public class WindowDecorActionBar extends ActionBar implements } public View getCustomView() { - return mActionView.getCustomNavigationView(); + return mDecorToolbar.getCustomView(); } public CharSequence getTitle() { - return mActionView.getTitle(); + return mDecorToolbar.getTitle(); } public CharSequence getSubtitle() { - return mActionView.getSubtitle(); + return mDecorToolbar.getSubtitle(); } public int getNavigationMode() { - return mActionView.getNavigationMode(); + return mDecorToolbar.getNavigationMode(); } public int getDisplayOptions() { - return mActionView.getDisplayOptions(); + return mDecorToolbar.getDisplayOptions(); } public ActionMode startActionMode(ActionMode.Callback callback) { @@ -572,7 +588,7 @@ public class WindowDecorActionBar extends ActionBar implements return; } - final FragmentTransaction trans = mActionView.isInEditMode() ? null : + final FragmentTransaction trans = mDecorToolbar.getViewGroup().isInEditMode() ? null : mActivity.getFragmentManager().beginTransaction().disallowAddToBackStack(); if (mSelectedTab == tab) { @@ -828,13 +844,18 @@ public class WindowDecorActionBar extends ActionBar implements hideForActionMode(); } - mActionView.animateToVisibility(toActionMode ? View.GONE : View.VISIBLE); + mDecorToolbar.animateToVisibility(toActionMode ? View.GONE : View.VISIBLE); mContextView.animateToVisibility(toActionMode ? View.VISIBLE : View.GONE); - if (mTabScrollView != null && !mActionView.hasEmbeddedTabs() && mActionView.isCollapsed()) { + if (mTabScrollView != null && !mDecorToolbar.hasEmbeddedTabs() && + isCollapsed(mDecorToolbar.getViewGroup())) { mTabScrollView.animateToVisibility(toActionMode ? View.GONE : View.VISIBLE); } } + private boolean isCollapsed(View view) { + return view == null || view.getVisibility() == View.GONE || view.getMeasuredHeight() == 0; + } + public Context getThemedContext() { if (mThemedContext == null) { TypedValue outValue = new TypedValue(); @@ -854,27 +875,27 @@ public class WindowDecorActionBar extends ActionBar implements @Override public boolean isTitleTruncated() { - return mActionView != null && mActionView.isTitleTruncated(); + return mDecorToolbar != null && mDecorToolbar.isTitleTruncated(); } @Override public void setHomeAsUpIndicator(Drawable indicator) { - mActionView.setHomeAsUpIndicator(indicator); + mDecorToolbar.setNavigationIcon(indicator); } @Override public void setHomeAsUpIndicator(int resId) { - mActionView.setHomeAsUpIndicator(resId); + mDecorToolbar.setNavigationIcon(resId); } @Override public void setHomeActionContentDescription(CharSequence description) { - mActionView.setHomeActionContentDescription(description); + mDecorToolbar.setNavigationContentDescription(description); } @Override public void setHomeActionContentDescription(int resId) { - mActionView.setHomeActionContentDescription(resId); + mDecorToolbar.setNavigationContentDescription(resId); } @Override @@ -938,7 +959,8 @@ public class WindowDecorActionBar extends ActionBar implements // Clear out the context mode views after the animation finishes mContextView.closeMode(); - mActionView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + mDecorToolbar.getViewGroup().sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); mOverlayLayout.setHideOnContentScrollEnabled(mHideOnContentScroll); mActionMode = null; @@ -1178,28 +1200,27 @@ public class WindowDecorActionBar extends ActionBar implements @Override public void setCustomView(View view) { - mActionView.setCustomNavigationView(view); + mDecorToolbar.setCustomView(view); } @Override public void setCustomView(View view, LayoutParams layoutParams) { view.setLayoutParams(layoutParams); - mActionView.setCustomNavigationView(view); + mDecorToolbar.setCustomView(view); } @Override public void setListNavigationCallbacks(SpinnerAdapter adapter, OnNavigationListener callback) { - mActionView.setDropdownAdapter(adapter); - mActionView.setCallback(callback); + mDecorToolbar.setDropdownParams(adapter, new NavItemSelectedListener(callback)); } @Override public int getSelectedNavigationIndex() { - switch (mActionView.getNavigationMode()) { + switch (mDecorToolbar.getNavigationMode()) { case NAVIGATION_MODE_TABS: return mSelectedTab != null ? mSelectedTab.getPosition() : -1; case NAVIGATION_MODE_LIST: - return mActionView.getDropdownSelectedPosition(); + return mDecorToolbar.getDropdownSelectedPosition(); default: return -1; } @@ -1207,12 +1228,11 @@ public class WindowDecorActionBar extends ActionBar implements @Override public int getNavigationItemCount() { - switch (mActionView.getNavigationMode()) { + switch (mDecorToolbar.getNavigationMode()) { case NAVIGATION_MODE_TABS: return mTabs.size(); case NAVIGATION_MODE_LIST: - SpinnerAdapter adapter = mActionView.getDropdownAdapter(); - return adapter != null ? adapter.getCount() : 0; + return mDecorToolbar.getDropdownItemCount(); default: return 0; } @@ -1225,7 +1245,7 @@ public class WindowDecorActionBar extends ActionBar implements @Override public void setNavigationMode(int mode) { - final int oldMode = mActionView.getNavigationMode(); + final int oldMode = mDecorToolbar.getNavigationMode(); switch (oldMode) { case NAVIGATION_MODE_TABS: mSavedTabPosition = getSelectedNavigationIndex(); @@ -1238,7 +1258,7 @@ public class WindowDecorActionBar extends ActionBar implements mOverlayLayout.requestFitSystemWindows(); } } - mActionView.setNavigationMode(mode); + mDecorToolbar.setNavigationMode(mode); switch (mode) { case NAVIGATION_MODE_TABS: ensureTabsExist(); @@ -1249,7 +1269,7 @@ public class WindowDecorActionBar extends ActionBar implements } break; } - mActionView.setCollapsable(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); + mDecorToolbar.setCollapsible(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); mOverlayLayout.setHasNonEmbeddedTabs(mode == NAVIGATION_MODE_TABS && !mHasEmbeddedTabs); } @@ -1261,30 +1281,30 @@ public class WindowDecorActionBar extends ActionBar implements @Override public void setIcon(int resId) { - mActionView.setIcon(resId); + mDecorToolbar.setIcon(resId); } @Override public void setIcon(Drawable icon) { - mActionView.setIcon(icon); + mDecorToolbar.setIcon(icon); } public boolean hasIcon() { - return mActionView.hasIcon(); + return mDecorToolbar.hasIcon(); } @Override public void setLogo(int resId) { - mActionView.setLogo(resId); + mDecorToolbar.setLogo(resId); } @Override public void setLogo(Drawable logo) { - mActionView.setLogo(logo); + mDecorToolbar.setLogo(logo); } public boolean hasLogo() { - return mActionView.hasLogo(); + return mDecorToolbar.hasLogo(); } public void setDefaultDisplayHomeAsUpEnabled(boolean enable) { @@ -1292,4 +1312,24 @@ public class WindowDecorActionBar extends ActionBar implements setDisplayHomeAsUpEnabled(enable); } } + + static class NavItemSelectedListener implements AdapterView.OnItemSelectedListener { + private final OnNavigationListener mListener; + + public NavItemSelectedListener(OnNavigationListener listener) { + mListener = listener; + } + + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + if (mListener != null) { + mListener.onNavigationItemSelected(position, id); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + // Do nothing + } + } } diff --git a/core/java/com/android/internal/backup/BackupConstants.java b/core/java/com/android/internal/backup/BackupConstants.java deleted file mode 100644 index 4c276b7..0000000 --- a/core/java/com/android/internal/backup/BackupConstants.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.backup; - -/** - * Constants used internally between the backup manager and its transports - */ -public class BackupConstants { - public static final int TRANSPORT_OK = 0; - public static final int TRANSPORT_ERROR = 1; - public static final int TRANSPORT_NOT_INITIALIZED = 2; - public static final int AGENT_ERROR = 3; - public static final int AGENT_UNKNOWN = 4; -} diff --git a/core/java/com/android/internal/backup/IBackupTransport.aidl b/core/java/com/android/internal/backup/IBackupTransport.aidl index 1e37fd9..d10451b 100644 --- a/core/java/com/android/internal/backup/IBackupTransport.aidl +++ b/core/java/com/android/internal/backup/IBackupTransport.aidl @@ -178,7 +178,7 @@ interface IBackupTransport { /** * Get the data for the application returned by {@link #nextRestorePackage}. * @param data An open, writable file into which the backup data should be stored. - * @return the same error codes as {@link #nextRestorePackage}. + * @return the same error codes as {@link #startRestore}. */ int getRestoreData(in ParcelFileDescriptor outFd); diff --git a/core/java/com/android/internal/backup/LocalTransport.java b/core/java/com/android/internal/backup/LocalTransport.java index 446ef55..7292116 100644 --- a/core/java/com/android/internal/backup/LocalTransport.java +++ b/core/java/com/android/internal/backup/LocalTransport.java @@ -18,6 +18,7 @@ package com.android.internal.backup; import android.app.backup.BackupDataInput; import android.app.backup.BackupDataOutput; +import android.app.backup.BackupTransport; import android.app.backup.RestoreSet; import android.content.ComponentName; import android.content.Context; @@ -47,7 +48,7 @@ import static android.system.OsConstants.*; * later restoring from there. For testing only. */ -public class LocalTransport extends IBackupTransport.Stub { +public class LocalTransport extends BackupTransport { private static final String TAG = "LocalTransport"; private static final boolean DEBUG = true; @@ -103,7 +104,7 @@ public class LocalTransport extends IBackupTransport.Stub { public int initializeDevice() { if (DEBUG) Log.v(TAG, "wiping all data"); deleteContents(mCurrentSetDir); - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor data) { @@ -165,7 +166,7 @@ public class LocalTransport extends IBackupTransport.Stub { entity.write(buf, 0, dataSize); } catch (IOException e) { Log.e(TAG, "Unable to update key file " + entityFile.getAbsolutePath()); - return BackupConstants.TRANSPORT_ERROR; + return BackupTransport.TRANSPORT_ERROR; } finally { entity.close(); } @@ -173,11 +174,11 @@ public class LocalTransport extends IBackupTransport.Stub { entityFile.delete(); } } - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } catch (IOException e) { // oops, something went wrong. abort the operation and return error. Log.v(TAG, "Exception reading backup input:", e); - return BackupConstants.TRANSPORT_ERROR; + return BackupTransport.TRANSPORT_ERROR; } } @@ -207,17 +208,17 @@ public class LocalTransport extends IBackupTransport.Stub { } packageDir.delete(); } - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } public int finishBackup() { if (DEBUG) Log.v(TAG, "finishBackup()"); - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } // Restore handling static final long[] POSSIBLE_SETS = { 2, 3, 4, 5, 6, 7, 8, 9 }; - public RestoreSet[] getAvailableRestoreSets() throws android.os.RemoteException { + public RestoreSet[] getAvailableRestoreSets() { long[] existing = new long[POSSIBLE_SETS.length + 1]; int num = 0; @@ -248,7 +249,7 @@ public class LocalTransport extends IBackupTransport.Stub { mRestorePackage = -1; mRestoreToken = token; mRestoreDataDir = new File(mDataDir, Long.toString(token)); - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } public String nextRestorePackage() { @@ -280,7 +281,7 @@ public class LocalTransport extends IBackupTransport.Stub { ArrayList<DecodedFilename> blobs = contentsByKey(packageDir); if (blobs == null) { // nextRestorePackage() ensures the dir exists, so this is an error Log.e(TAG, "No keys for package: " + packageDir); - return BackupConstants.TRANSPORT_ERROR; + return BackupTransport.TRANSPORT_ERROR; } // We expect at least some data if the directory exists in the first place @@ -301,10 +302,10 @@ public class LocalTransport extends IBackupTransport.Stub { in.close(); } } - return BackupConstants.TRANSPORT_OK; + return BackupTransport.TRANSPORT_OK; } catch (IOException e) { Log.e(TAG, "Unable to read backup records", e); - return BackupConstants.TRANSPORT_ERROR; + return BackupTransport.TRANSPORT_ERROR; } } diff --git a/core/java/com/android/internal/backup/LocalTransportService.java b/core/java/com/android/internal/backup/LocalTransportService.java index d05699a..77ac313 100644 --- a/core/java/com/android/internal/backup/LocalTransportService.java +++ b/core/java/com/android/internal/backup/LocalTransportService.java @@ -32,6 +32,6 @@ public class LocalTransportService extends Service { @Override public IBinder onBind(Intent intent) { - return sTransport; + return sTransport.getBinder(); } } diff --git a/core/java/com/android/internal/content/NativeLibraryHelper.java b/core/java/com/android/internal/content/NativeLibraryHelper.java index ba419f9..dab3aff 100644 --- a/core/java/com/android/internal/content/NativeLibraryHelper.java +++ b/core/java/com/android/internal/content/NativeLibraryHelper.java @@ -20,6 +20,7 @@ import android.content.pm.PackageManager; import android.util.Slog; import java.io.File; +import java.io.IOException; /** * Native libraries helper. @@ -141,4 +142,18 @@ public class NativeLibraryHelper { return deletedFiles; } + + // We don't care about the other return values for now. + private static final int BITCODE_PRESENT = 1; + + public static boolean hasRenderscriptBitcode(ApkHandle handle) throws IOException { + final int returnVal = hasRenderscriptBitcode(handle.apkHandle); + if (returnVal < 0) { + throw new IOException("Error scanning APK, code: " + returnVal); + } + + return (returnVal == BITCODE_PRESENT); + } + + private static native int hasRenderscriptBitcode(long apkHandle); } diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java index e3f21cf..7dbde69 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java @@ -16,17 +16,17 @@ 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.Log; import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; -import java.util.ArrayDeque; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.inputmethod.InputMethodUtils.InputMethodSettings; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -34,32 +34,21 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.TreeMap; /** * InputMethodSubtypeSwitchingController controls the switching behavior of the subtypes. + * <p> + * This class is designed to be used from and only from {@link InputMethodManagerService} by using + * {@link InputMethodManagerService#mMethodMap} as a global lock. + * </p> */ 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; @@ -118,6 +107,35 @@ public class InputMethodSubtypeSwitchingController { } return mSubtypeName.toString().compareTo(other.mSubtypeName.toString()); } + + @Override + public String toString() { + return "ImeSubtypeListItem{" + + "mImeName=" + mImeName + + " mSubtypeName=" + mSubtypeName + + " mSubtypeId=" + mSubtypeId + + " mIsSystemLocale=" + mIsSystemLocale + + " mIsSystemLanguage=" + mIsSystemLanguage + + "}"; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o instanceof ImeSubtypeListItem) { + final ImeSubtypeListItem that = (ImeSubtypeListItem)o; + if (!Objects.equals(this.mImi, that.mImi)) { + return false; + } + if (this.mSubtypeId != that.mSubtypeId) { + return false; + } + return true; + } + return false; + } } private static class InputMethodAndSubtypeList { @@ -213,102 +231,273 @@ public class InputMethodSubtypeSwitchingController { } } - private final ArrayDeque<SubtypeParams> mTypedSubtypeHistory = new ArrayDeque<SubtypeParams>(); - private final Object mLock = new Object(); - private final InputMethodSettings mSettings; - private InputMethodAndSubtypeList mSubtypeList; + private static int calculateSubtypeId(InputMethodInfo imi, InputMethodSubtype subtype) { + return subtype != null ? InputMethodUtils.getSubtypeIdFromHashCode(imi, + subtype.hashCode()) : NOT_A_SUBTYPE_ID; + } - @VisibleForTesting - public static ImeSubtypeListItem getNextInputMethodImpl(List<ImeSubtypeListItem> imList, - boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { - if (imi == null) { - return null; + private static class StaticRotationList { + private final List<ImeSubtypeListItem> mImeSubtypeList; + public StaticRotationList(final List<ImeSubtypeListItem> imeSubtypeList) { + mImeSubtypeList = imeSubtypeList; } - if (imList.size() <= 1) { - return null; - } - // Here we have two rotation groups, depending on the returned boolean value of - // {@link InputMethodInfo#supportsSwitchingToNextInputMethod()}. - final boolean expectedValueOfSupportsSwitchingToNextInputMethod = - imi.supportsSwitchingToNextInputMethod(); - 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); - // Skip until the current IME/subtype is found. - if (!isli.mImi.equals(imi) || isli.mSubtypeId != currentSubtypeId) { - continue; - } - // Found the current IME/subtype. Start searching the next IME/subtype from here. - for (int j = 0; j < N - 1; ++j) { - final ImeSubtypeListItem candidate = imList.get((i + j + 1) % N); - // Skip if the candidate doesn't belong to the expected rotation group. - if (expectedValueOfSupportsSwitchingToNextInputMethod != - candidate.mImi.supportsSwitchingToNextInputMethod()) { - continue; + + /** + * Returns the index of the specified input method and subtype in the given list. + * @param imi The {@link InputMethodInfo} to be searched. + * @param subtype The {@link InputMethodSubtype} to be searched. null if the input method + * does not have a subtype. + * @return The index in the given list. -1 if not found. + */ + private int getIndex(InputMethodInfo imi, InputMethodSubtype subtype) { + final int currentSubtypeId = calculateSubtypeId(imi, subtype); + final int N = mImeSubtypeList.size(); + for (int i = 0; i < N; ++i) { + final ImeSubtypeListItem isli = mImeSubtypeList.get(i); + // Skip until the current IME/subtype is found. + if (imi.equals(isli.mImi) && isli.mSubtypeId == currentSubtypeId) { + return i; } + } + return -1; + } + + public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, + InputMethodInfo imi, InputMethodSubtype subtype) { + if (imi == null) { + return null; + } + if (mImeSubtypeList.size() <= 1) { + return null; + } + final int currentIndex = getIndex(imi, subtype); + if (currentIndex < 0) { + return null; + } + final int N = mImeSubtypeList.size(); + for (int offset = 1; offset < N; ++offset) { + // Start searching the next IME/subtype from the next of the current index. + final int candidateIndex = (currentIndex + offset) % N; + final ImeSubtypeListItem candidate = mImeSubtypeList.get(candidateIndex); // Skip if searching inside the current IME only, but the candidate is not // the current IME. - if (onlyCurrentIme && !candidate.mImi.equals(imi)) { + if (onlyCurrentIme && !imi.equals(candidate.mImi)) { continue; } return candidate; } - // No appropriate IME/subtype is found in the list. Give up. return null; } - // The current IME/subtype is not found in the list. Give up. - return null; } - public InputMethodSubtypeSwitchingController(InputMethodSettings settings) { - mSettings = settings; + private static class DynamicRotationList { + private static final String TAG = DynamicRotationList.class.getSimpleName(); + private final List<ImeSubtypeListItem> mImeSubtypeList; + private final int[] mUsageHistoryOfSubtypeListItemIndex; + + private DynamicRotationList(final List<ImeSubtypeListItem> imeSubtypeListItems) { + mImeSubtypeList = imeSubtypeListItems; + mUsageHistoryOfSubtypeListItemIndex = new int[mImeSubtypeList.size()]; + final int N = mImeSubtypeList.size(); + for (int i = 0; i < N; i++) { + mUsageHistoryOfSubtypeListItemIndex[i] = i; + } + } + + /** + * Returns the index of the specified object in + * {@link #mUsageHistoryOfSubtypeListItemIndex}. + * <p>We call the index of {@link #mUsageHistoryOfSubtypeListItemIndex} as "Usage Rank" + * so as not to be confused with the index in {@link #mImeSubtypeList}. + * @return -1 when the specified item doesn't belong to {@link #mImeSubtypeList} actually. + */ + private int getUsageRank(final InputMethodInfo imi, InputMethodSubtype subtype) { + final int currentSubtypeId = calculateSubtypeId(imi, subtype); + final int N = mUsageHistoryOfSubtypeListItemIndex.length; + for (int usageRank = 0; usageRank < N; usageRank++) { + final int subtypeListItemIndex = mUsageHistoryOfSubtypeListItemIndex[usageRank]; + final ImeSubtypeListItem subtypeListItem = + mImeSubtypeList.get(subtypeListItemIndex); + if (subtypeListItem.mImi.equals(imi) && + subtypeListItem.mSubtypeId == currentSubtypeId) { + return usageRank; + } + } + // Not found in the known IME/Subtype list. + return -1; + } + + public void onUserAction(InputMethodInfo imi, InputMethodSubtype subtype) { + final int currentUsageRank = getUsageRank(imi, subtype); + // Do nothing if currentUsageRank == -1 (not found), or currentUsageRank == 0 + if (currentUsageRank <= 0) { + return; + } + final int currentItemIndex = mUsageHistoryOfSubtypeListItemIndex[currentUsageRank]; + System.arraycopy(mUsageHistoryOfSubtypeListItemIndex, 0, + mUsageHistoryOfSubtypeListItemIndex, 1, currentUsageRank); + mUsageHistoryOfSubtypeListItemIndex[0] = currentItemIndex; + } + + public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, + InputMethodInfo imi, InputMethodSubtype subtype) { + int currentUsageRank = getUsageRank(imi, subtype); + if (currentUsageRank < 0) { + if (DEBUG) { + Slog.d(TAG, "IME/subtype is not found: " + imi.getId() + ", " + subtype); + } + return null; + } + final int N = mUsageHistoryOfSubtypeListItemIndex.length; + for (int i = 1; i < N; i++) { + final int subtypeListItemRank = (currentUsageRank + i) % N; + final int subtypeListItemIndex = + mUsageHistoryOfSubtypeListItemIndex[subtypeListItemRank]; + final ImeSubtypeListItem subtypeListItem = + mImeSubtypeList.get(subtypeListItemIndex); + if (onlyCurrentIme && !imi.equals(subtypeListItem.mImi)) { + continue; + } + return subtypeListItem; + } + return null; + } } - // 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); + @VisibleForTesting + public static class ControllerImpl { + private final DynamicRotationList mSwitchingAwareRotationList; + private final StaticRotationList mSwitchingUnawareRotationList; + + public static ControllerImpl createFrom(final ControllerImpl currentInstance, + final List<ImeSubtypeListItem> sortedEnabledItems) { + DynamicRotationList switchingAwareRotationList = null; + { + final List<ImeSubtypeListItem> switchingAwareImeSubtypes = + filterImeSubtypeList(sortedEnabledItems, + true /* supportsSwitchingToNextInputMethod */); + if (currentInstance != null && + currentInstance.mSwitchingAwareRotationList != null && + Objects.equals(currentInstance.mSwitchingAwareRotationList.mImeSubtypeList, + switchingAwareImeSubtypes)) { + // Can reuse the current instance. + switchingAwareRotationList = currentInstance.mSwitchingAwareRotationList; + } + if (switchingAwareRotationList == null) { + switchingAwareRotationList = new DynamicRotationList(switchingAwareImeSubtypes); + } + } + + StaticRotationList switchingUnawareRotationList = null; + { + final List<ImeSubtypeListItem> switchingUnawareImeSubtypes = filterImeSubtypeList( + sortedEnabledItems, false /* supportsSwitchingToNextInputMethod */); + if (currentInstance != null && + currentInstance.mSwitchingUnawareRotationList != null && + Objects.equals( + currentInstance.mSwitchingUnawareRotationList.mImeSubtypeList, + switchingUnawareImeSubtypes)) { + // Can reuse the current instance. + switchingUnawareRotationList = currentInstance.mSwitchingUnawareRotationList; + } + if (switchingUnawareRotationList == null) { + switchingUnawareRotationList = + new StaticRotationList(switchingUnawareImeSubtypes); + } + } + + return new ControllerImpl(switchingAwareRotationList, switchingUnawareRotationList); + } + + private ControllerImpl(final DynamicRotationList switchingAwareRotationList, + final StaticRotationList switchingUnawareRotationList) { + mSwitchingAwareRotationList = switchingAwareRotationList; + mSwitchingUnawareRotationList = switchingUnawareRotationList; + } + + public ImeSubtypeListItem getNextInputMethod(boolean onlyCurrentIme, InputMethodInfo imi, + InputMethodSubtype subtype) { + if (imi == null) { + return null; + } + if (imi.supportsSwitchingToNextInputMethod()) { + return mSwitchingAwareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, + subtype); + } else { + return mSwitchingUnawareRotationList.getNextInputMethodLocked(onlyCurrentIme, imi, + subtype); + } + } + + public void onUserActionLocked(InputMethodInfo imi, InputMethodSubtype subtype) { + if (imi == null) { return; } - if (DEBUG) { - Slog.d(TAG, "onCommitText: " + imi.getId() + ", " + subtype); + if (imi.supportsSwitchingToNextInputMethod()) { + mSwitchingAwareRotationList.onUserAction(imi, subtype); } - if (REQUIRE_SWITCHING_SUPPORT) { - if (!imi.supportsSwitchingToNextInputMethod()) { - Slog.w(TAG, imi.getId() + " doesn't support switching to next input method."); - return; + } + + private static List<ImeSubtypeListItem> filterImeSubtypeList( + final List<ImeSubtypeListItem> items, + final boolean supportsSwitchingToNextInputMethod) { + final ArrayList<ImeSubtypeListItem> result = new ArrayList<>(); + final int ALL_ITEMS_COUNT = items.size(); + for (int i = 0; i < ALL_ITEMS_COUNT; i++) { + final ImeSubtypeListItem item = items.get(i); + if (item.mImi.supportsSwitchingToNextInputMethod() == + supportsSwitchingToNextInputMethod) { + result.add(item); } } - if (mTypedSubtypeHistory.size() >= MAX_HISTORY_SIZE) { - mTypedSubtypeHistory.poll(); + return result; + } + } + + private final InputMethodSettings mSettings; + private InputMethodAndSubtypeList mSubtypeList; + private ControllerImpl mController; + + private InputMethodSubtypeSwitchingController(InputMethodSettings settings, Context context) { + mSettings = settings; + resetCircularListLocked(context); + } + + public static InputMethodSubtypeSwitchingController createInstanceLocked( + InputMethodSettings settings, Context context) { + return new InputMethodSubtypeSwitchingController(settings, context); + } + + public void onCommitTextLocked(InputMethodInfo imi, InputMethodSubtype subtype) { + if (mController == null) { + if (DEBUG) { + Log.e(TAG, "mController shouldn't be null."); } - mTypedSubtypeHistory.addFirst(new SubtypeParams(imi, subtype)); + return; } + mController.onUserActionLocked(imi, subtype); } public void resetCircularListLocked(Context context) { - synchronized(mLock) { - mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); - } + mSubtypeList = new InputMethodAndSubtypeList(context, mSettings); + mController = ControllerImpl.createFrom(mController, + mSubtypeList.getSortedInputMethodAndSubtypeList()); } - public ImeSubtypeListItem getNextInputMethod( - boolean onlyCurrentIme, InputMethodInfo imi, InputMethodSubtype subtype) { - synchronized(mLock) { - return getNextInputMethodImpl(mSubtypeList.getSortedInputMethodAndSubtypeList(), - onlyCurrentIme, imi, subtype); + public ImeSubtypeListItem getNextInputMethodLocked(boolean onlyCurrentIme, InputMethodInfo imi, + InputMethodSubtype subtype) { + if (mController == null) { + if (DEBUG) { + Log.e(TAG, "mController shouldn't be null."); + } + return null; } + return mController.getNextInputMethod(onlyCurrentIme, imi, subtype); } - public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList(boolean showSubtypes, + public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { - synchronized(mLock) { - return mSubtypeList.getSortedInputMethodAndSubtypeList( - showSubtypes, inputShown, isScreenLocked); - } + return mSubtypeList.getSortedInputMethodAndSubtypeList( + showSubtypes, inputShown, isScreenLocked); } } diff --git a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java b/core/java/com/android/internal/notification/DemoContactNotificationScorer.java deleted file mode 100644 index f484724..0000000 --- a/core/java/com/android/internal/notification/DemoContactNotificationScorer.java +++ /dev/null @@ -1,188 +0,0 @@ -/* -* 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.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.Settings; -import android.text.SpannableString; -import android.util.Slog; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -/** - * This NotificationScorer bumps up the priority of notifications that contain references to the - * display names of starred contacts. The references it picks up are spannable strings which, in - * their entirety, match the display name of some starred contact. The magnitude of the bump ranges - * from 0 to 15 (assuming NOTIFICATION_PRIORITY_MULTIPLIER = 10) depending on the initial score, and - * the mapping is defined by priorityBumpMap. In a production version of this scorer, a notification - * extra will be used to specify contact identifiers. - */ - -public class DemoContactNotificationScorer implements NotificationScorer { - private static final String TAG = "DemoContactNotificationScorer"; - private static final boolean DBG = false; - - protected static final boolean ENABLE_CONTACT_SCORER = true; - private static final String SETTING_ENABLE_SCORER = "contact_scorer_enabled"; - protected boolean mEnabled; - - // see NotificationManagerService - private static final int NOTIFICATION_PRIORITY_MULTIPLIER = 10; - - private Context mContext; - - private static final List<String> RELEVANT_KEYS_LIST = Arrays.asList( - Notification.EXTRA_INFO_TEXT, Notification.EXTRA_TEXT, Notification.EXTRA_TEXT_LINES, - Notification.EXTRA_SUB_TEXT, Notification.EXTRA_TITLE - ); - - private static final String[] PROJECTION = new String[] { - ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME - }; - - private static final Uri CONTACTS_URI = ContactsContract.Contacts.CONTENT_URI; - - private static List<String> extractSpannedStrings(CharSequence charSequence) { - if (charSequence == null) return Collections.emptyList(); - if (!(charSequence instanceof SpannableString)) { - return Arrays.asList(charSequence.toString()); - } - SpannableString spannableString = (SpannableString)charSequence; - // get all spans - Object[] ssArr = spannableString.getSpans(0, spannableString.length(), Object.class); - // spanned string sequences - ArrayList<String> sss = new ArrayList<String>(); - for (Object spanObj : ssArr) { - try { - sss.add(spannableString.subSequence(spannableString.getSpanStart(spanObj), - spannableString.getSpanEnd(spanObj)).toString()); - } catch(StringIndexOutOfBoundsException e) { - Slog.e(TAG, "Bad indices when extracting spanned subsequence", e); - } - } - return sss; - }; - - private static String getQuestionMarksInParens(int n) { - StringBuilder sb = new StringBuilder("("); - for (int i = 0; i < n; i++) { - if (sb.length() > 1) sb.append(','); - sb.append('?'); - } - sb.append(")"); - return sb.toString(); - } - - private boolean hasStarredContact(Bundle extras) { - if (extras == null) return false; - ArrayList<String> qStrings = new ArrayList<String>(); - // build list to query against the database for display names. - for (String rk: RELEVANT_KEYS_LIST) { - if (extras.get(rk) == null) { - continue; - } else if (extras.get(rk) instanceof CharSequence) { - qStrings.addAll(extractSpannedStrings((CharSequence) extras.get(rk))); - } else if (extras.get(rk) instanceof CharSequence[]) { - // this is intended for Notification.EXTRA_TEXT_LINES - for (CharSequence line: (CharSequence[]) extras.get(rk)){ - qStrings.addAll(extractSpannedStrings(line)); - } - } else { - Slog.w(TAG, "Strange, the extra " + rk + " is of unexpected type."); - } - } - if (qStrings.isEmpty()) return false; - String[] qStringsArr = qStrings.toArray(new String[qStrings.size()]); - - String selection = ContactsContract.Contacts.DISPLAY_NAME + " IN " - + getQuestionMarksInParens(qStringsArr.length) + " AND " - + ContactsContract.Contacts.STARRED+" ='1'"; - - Cursor c = null; - try { - c = mContext.getContentResolver().query( - CONTACTS_URI, PROJECTION, selection, qStringsArr, null); - if (c != null) return c.getCount() > 0; - } catch(Throwable t) { - Slog.w(TAG, "Problem getting content resolver or performing contacts query.", t); - } finally { - if (c != null) { - c.close(); - } - } - return false; - } - - private final static int clamp(int x, int low, int high) { - return (x < low) ? low : ((x > high) ? high : x); - } - - 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; - mEnabled = ENABLE_CONTACT_SCORER && 1 == Settings.Global.getInt( - mContext.getContentResolver(), SETTING_ENABLE_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; - } - boolean hasStarredPriority = hasStarredContact(notification.extras); - - if (DBG) { - if (hasStarredPriority) { - Slog.v(TAG, "Notification references starred contact. Promoted!"); - } else { - Slog.v(TAG, "Notification lacks any starred contact reference. Not promoted!"); - } - } - if (hasStarredPriority) score = priorityBumpMap(score); - return score; - } -} - diff --git a/core/java/com/android/internal/notification/NotificationScorer.java b/core/java/com/android/internal/notification/NotificationScorer.java deleted file mode 100644 index 863c08c..0000000 --- a/core/java/com/android/internal/notification/NotificationScorer.java +++ /dev/null @@ -1,27 +0,0 @@ -/* -* 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.notification; - -import android.app.Notification; -import android.content.Context; - -public interface NotificationScorer { - - public void initialize(Context context); - public int getScore(Notification notification, int score); - -} diff --git a/core/java/com/android/internal/notification/PeopleNotificationScorer.java b/core/java/com/android/internal/notification/PeopleNotificationScorer.java deleted file mode 100644 index efb5f63..0000000 --- a/core/java/com/android/internal/notification/PeopleNotificationScorer.java +++ /dev/null @@ -1,227 +0,0 @@ -/* -* 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/BatteryStatsImpl.java b/core/java/com/android/internal/os/BatteryStatsImpl.java index 1aff190..240d520 100644 --- a/core/java/com/android/internal/os/BatteryStatsImpl.java +++ b/core/java/com/android/internal/os/BatteryStatsImpl.java @@ -48,7 +48,6 @@ 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 android.view.Display; @@ -89,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 = 105 + (USE_OLD_HISTORY ? 1000 : 0); + private static final int VERSION = 106 + (USE_OLD_HISTORY ? 1000 : 0); // Maximum number of items we will record in the history. private static final int MAX_HISTORY_ITEMS = 2000; @@ -188,8 +187,7 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mShuttingDown; - HashMap<String, SparseBooleanArray>[] mActiveEvents - = (HashMap<String, SparseBooleanArray>[]) new HashMap[HistoryItem.EVENT_COUNT]; + final HistoryEventTracker mActiveEvents = new HistoryEventTracker(); long mHistoryBaseTime; boolean mHaveBatteryLevel = false; @@ -237,6 +235,8 @@ public final class BatteryStatsImpl extends BatteryStats { int mWakeLockNesting; boolean mWakeLockImportant; + boolean mRecordAllWakeLocks; + boolean mNoAutoReset; int mScreenState = Display.STATE_UNKNOWN; StopwatchTimer mScreenOnTimer; @@ -247,6 +247,9 @@ public final class BatteryStatsImpl extends BatteryStats { boolean mInteractive; StopwatchTimer mInteractiveTimer; + boolean mLowPowerModeEnabled; + StopwatchTimer mLowPowerModeEnabledTimer; + boolean mPhoneOn; StopwatchTimer mPhoneOnTimer; @@ -325,11 +328,13 @@ public final class BatteryStatsImpl extends BatteryStats { int mLastDischargeStepLevel; long mLastDischargeStepTime; + int mMinDischargeStepLevel; int mNumDischargeStepDurations; final long[] mDischargeStepDurations = new long[MAX_LEVEL_STEPS]; int mLastChargeStepLevel; long mLastChargeStepTime; + int mMaxChargeStepLevel; int mNumChargeStepDurations; final long[] mChargeStepDurations = new long[MAX_LEVEL_STEPS]; @@ -884,6 +889,7 @@ public final class BatteryStatsImpl extends BatteryStats { mLastTime = 0; mUnpluggedTime = in.readLong(); timeBase.add(this); + if (DEBUG) Log.i(TAG, "**** READ TIMER #" + mType + ": mTotalTime=" + mTotalTime); } Timer(int type, TimeBase timeBase) { @@ -914,6 +920,8 @@ public final class BatteryStatsImpl extends BatteryStats { } public void writeToParcel(Parcel out, long elapsedRealtimeUs) { + if (DEBUG) Log.i(TAG, "**** WRITING TIMER #" + mType + ": mTotalTime=" + + computeRunTimeLocked(mTimeBase.getRealtime(elapsedRealtimeUs))); out.writeInt(mCount); out.writeInt(mLoadedCount); out.writeInt(mUnpluggedCount); @@ -1998,6 +2006,11 @@ public final class BatteryStatsImpl extends BatteryStats { } } + @Override + public void commitCurrentHistoryBatchLocked() { + mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + } + void addHistoryBufferLocked(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) { if (!mHaveBatteryLevel || !mRecordingHistory) { return; @@ -2297,44 +2310,8 @@ public final class BatteryStatsImpl extends BatteryStats { 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); - } + if (!mActiveEvents.updateState(code, name, uid, 0)) { + return; } final long elapsedRealtime = SystemClock.elapsedRealtime(); final long uptime = SystemClock.uptimeMillis(); @@ -2348,6 +2325,21 @@ public final class BatteryStatsImpl extends BatteryStats { } } + public void setRecordAllWakeLocksLocked(boolean enabled) { + mRecordAllWakeLocks = enabled; + if (!enabled) { + // Clear out any existing state. + mActiveEvents.removeEvents(HistoryItem.EVENT_WAKE_LOCK); + } + } + + public void setNoAutoReset(boolean enabled) { + mNoAutoReset = enabled; + } + + private String mInitialAcquireWakeName; + private int mInitialAcquireWakeUid = -1; + public void noteStartWakeLocked(int uid, int pid, String name, String historyName, int type, boolean unimportantForLogging, long elapsedRealtime, long uptime) { uid = mapUid(uid); @@ -2355,22 +2347,33 @@ public final class BatteryStatsImpl extends BatteryStats { // Only care about partial wake locks, since full wake locks // will be canceled when the user puts the screen to sleep. aggregateLastWakeupUptimeLocked(uptime); + if (historyName == null) { + historyName = name; + } + if (mRecordAllWakeLocks) { + if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_START, historyName, + uid, 0)) { + addHistoryEventLocked(elapsedRealtime, uptime, + HistoryItem.EVENT_WAKE_LOCK_START, historyName, uid); + } + } if (mWakeLockNesting == 0) { mHistoryCur.states |= HistoryItem.STATE_WAKE_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Start wake lock to: " + Integer.toHexString(mHistoryCur.states)); mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; - mHistoryCur.wakelockTag.string = historyName != null ? historyName : name; - mHistoryCur.wakelockTag.uid = uid; + mHistoryCur.wakelockTag.string = mInitialAcquireWakeName = historyName; + mHistoryCur.wakelockTag.uid = mInitialAcquireWakeUid = uid; mWakeLockImportant = !unimportantForLogging; addHistoryRecordLocked(elapsedRealtime, uptime); - } else if (!mWakeLockImportant && !unimportantForLogging) { + } else if (!mWakeLockImportant && !unimportantForLogging + && mHistoryLastWritten.cmd == HistoryItem.CMD_UPDATE) { 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; + mHistoryCur.wakelockTag.string = mInitialAcquireWakeName = historyName; + mHistoryCur.wakelockTag.uid = mInitialAcquireWakeUid = uid; addHistoryRecordLocked(elapsedRealtime, uptime); } mWakeLockImportant = true; @@ -2386,15 +2389,27 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public void noteStopWakeLocked(int uid, int pid, String name, int type, long elapsedRealtime, - long uptime) { + public void noteStopWakeLocked(int uid, int pid, String name, String historyName, int type, + long elapsedRealtime, long uptime) { uid = mapUid(uid); if (type == WAKE_TYPE_PARTIAL) { mWakeLockNesting--; + if (mRecordAllWakeLocks) { + if (historyName == null) { + historyName = name; + } + if (mActiveEvents.updateState(HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName, + uid, 0)) { + addHistoryEventLocked(elapsedRealtime, uptime, + HistoryItem.EVENT_WAKE_LOCK_FINISH, historyName, uid); + } + } if (mWakeLockNesting == 0) { mHistoryCur.states &= ~HistoryItem.STATE_WAKE_LOCK_FLAG; if (DEBUG_HISTORY) Slog.v(TAG, "Stop wake lock to: " + Integer.toHexString(mHistoryCur.states)); + mInitialAcquireWakeName = null; + mInitialAcquireWakeUid = -1; addHistoryRecordLocked(elapsedRealtime, uptime); } } @@ -2415,8 +2430,8 @@ public final class BatteryStatsImpl extends BatteryStats { } } - public void noteChangeWakelockFromSourceLocked(WorkSource ws, int pid, String name, int type, - WorkSource newWs, int newPid, String newName, + public void noteChangeWakelockFromSourceLocked(WorkSource ws, int pid, String name, + String historyName, int type, WorkSource newWs, int newPid, String newName, String newHistoryName, int newType, boolean newUnimportantForLogging) { final long elapsedRealtime = SystemClock.elapsedRealtime(); final long uptime = SystemClock.uptimeMillis(); @@ -2430,16 +2445,17 @@ public final class BatteryStatsImpl extends BatteryStats { } final int NO = ws.size(); for (int i=0; i<NO; i++) { - noteStopWakeLocked(ws.get(i), pid, name, type, elapsedRealtime, uptime); + noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime); } } - public void noteStopWakeFromSourceLocked(WorkSource ws, int pid, String name, int type) { + public void noteStopWakeFromSourceLocked(WorkSource ws, int pid, String name, + String historyName, int type) { 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, elapsedRealtime, uptime); + noteStopWakeLocked(ws.get(i), pid, name, historyName, type, elapsedRealtime, uptime); } } @@ -2708,7 +2724,7 @@ public final class BatteryStatsImpl extends BatteryStats { mScreenBrightnessTimer[mScreenBrightnessBin].stopRunningLocked(elapsedRealtime); } - noteStopWakeLocked(-1, -1, "screen", WAKE_TYPE_PARTIAL, + noteStopWakeLocked(-1, -1, "screen", "screen", WAKE_TYPE_PARTIAL, elapsedRealtime, uptime); updateTimeBasesLocked(mOnBatteryTimeBase.isRunning(), true, @@ -2804,6 +2820,26 @@ public final class BatteryStatsImpl extends BatteryStats { } } + public void noteLowPowerMode(boolean enabled) { + if (mLowPowerModeEnabled != enabled) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + mLowPowerModeEnabled = enabled; + if (enabled) { + mHistoryCur.states2 |= HistoryItem.STATE2_LOW_POWER_FLAG; + if (DEBUG_HISTORY) Slog.v(TAG, "Low power mode enabled to: " + + Integer.toHexString(mHistoryCur.states2)); + mLowPowerModeEnabledTimer.startRunningLocked(elapsedRealtime); + } else { + mHistoryCur.states2 &= ~HistoryItem.STATE2_LOW_POWER_FLAG; + if (DEBUG_HISTORY) Slog.v(TAG, "Low power mode disabled to: " + + Integer.toHexString(mHistoryCur.states2)); + mLowPowerModeEnabledTimer.stopRunningLocked(elapsedRealtime); + } + addHistoryRecordLocked(elapsedRealtime, uptime); + } + } + public void notePhoneOnLocked() { if (!mPhoneOn) { final long elapsedRealtime = SystemClock.elapsedRealtime(); @@ -3467,6 +3503,14 @@ public final class BatteryStatsImpl extends BatteryStats { return mInteractiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } + @Override public long getLowPowerModeEnabledTime(long elapsedRealtimeUs, int which) { + return mLowPowerModeEnabledTimer.getTotalTimeLocked(elapsedRealtimeUs, which); + } + + @Override public int getLowPowerModeEnabledCount(int which) { + return mLowPowerModeEnabledTimer.getCountLocked(which); + } + @Override public long getPhoneOnTime(long elapsedRealtimeUs, int which) { return mPhoneOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which); } @@ -5513,7 +5557,9 @@ public final class BatteryStatsImpl extends BatteryStats { for (int i=0; i<NUM_SCREEN_BRIGHTNESS_BINS; i++) { mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, null, mOnBatteryTimeBase); } - mPhoneOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase); + mInteractiveTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase); + mLowPowerModeEnabledTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase); + mPhoneOnTimer = new StopwatchTimer(null, -3, null, mOnBatteryTimeBase); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(null, -200-i, null, mOnBatteryTimeBase); @@ -5532,18 +5578,17 @@ public final class BatteryStatsImpl extends BatteryStats { 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); + mWifiOnTimer = new StopwatchTimer(null, -4, null, mOnBatteryTimeBase); + mGlobalWifiRunningTimer = new StopwatchTimer(null, -5, 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); + mBluetoothOnTimer = new StopwatchTimer(null, -6, 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); - mInteractiveTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase); + mAudioOnTimer = new StopwatchTimer(null, -7, null, mOnBatteryTimeBase); + mVideoOnTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase); mOnBattery = mOnBatteryInternal = false; long uptime = SystemClock.uptimeMillis() * 1000; long realtime = SystemClock.elapsedRealtime() * 1000; @@ -5699,7 +5744,8 @@ public final class BatteryStatsImpl extends BatteryStats { final long lastRealtime = out.time; final long lastWalltime = out.currentTime; readHistoryDelta(mHistoryBuffer, out); - if (out.cmd != HistoryItem.CMD_CURRENT_TIME && lastWalltime != 0) { + if (out.cmd != HistoryItem.CMD_CURRENT_TIME + && out.cmd != HistoryItem.CMD_RESET && lastWalltime != 0) { out.currentTime = lastWalltime + (out.time - lastRealtime); } return true; @@ -5788,6 +5834,7 @@ public final class BatteryStatsImpl extends BatteryStats { mScreenBrightnessTimer[i].reset(false); } mInteractiveTimer.reset(false); + mLowPowerModeEnabledTimer.reset(false); mPhoneOnTimer.reset(false); mAudioOnTimer.reset(false); mVideoOnTimer.reset(false); @@ -5845,17 +5892,15 @@ public final class BatteryStatsImpl extends BatteryStats { private void initActiveHistoryEventsLocked(long elapsedRealtimeMs, long uptimeMs) { for (int i=0; i<HistoryItem.EVENT_COUNT; i++) { - HashMap<String, SparseBooleanArray> active = mActiveEvents[i]; + HashMap<String, SparseIntArray> active = mActiveEvents.getStateForEvent(i); if (active == null) { continue; } - for (HashMap.Entry<String, SparseBooleanArray> ent : active.entrySet()) { - SparseBooleanArray uids = ent.getValue(); + for (HashMap.Entry<String, SparseIntArray> ent : active.entrySet()) { + SparseIntArray uids = ent.getValue(); for (int j=0; j<uids.size(); j++) { - if (uids.valueAt(j)) { - addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, i, ent.getKey(), - uids.keyAt(j)); - } + addHistoryEventLocked(elapsedRealtimeMs, uptimeMs, i, ent.getKey(), + uids.keyAt(j)); } } } @@ -5910,9 +5955,9 @@ public final class BatteryStatsImpl extends BatteryStats { // 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 + if (!mNoAutoReset && (oldStatus == BatteryManager.BATTERY_STATUS_FULL || level >= 90 - || (mDischargeCurrentLevel < 20 && level >= 80)) { + || (mDischargeCurrentLevel < 20 && level >= 80))) { doWrite = true; resetAllStatsLocked(); mDischargeStartLevel = level; @@ -5920,6 +5965,7 @@ public final class BatteryStatsImpl extends BatteryStats { mNumDischargeStepDurations = 0; } mLastDischargeStepLevel = level; + mMinDischargeStepLevel = level; mLastDischargeStepTime = -1; pullPendingStateUpdatesLocked(); mHistoryCur.batteryLevel = (byte)level; @@ -5958,6 +6004,7 @@ public final class BatteryStatsImpl extends BatteryStats { updateTimeBasesLocked(false, !screenOn, uptime, realtime); mNumChargeStepDurations = 0; mLastChargeStepLevel = level; + mMaxChargeStepLevel = level; mLastChargeStepTime = -1; } if (doWrite || (mLastWriteTime + (60 * 1000)) < mSecRealtime) { @@ -5971,7 +6018,8 @@ public final class BatteryStatsImpl extends BatteryStats { boolean reset) { mRecordingHistory = true; mHistoryCur.currentTime = System.currentTimeMillis(); - addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, HistoryItem.CMD_CURRENT_TIME, + addHistoryBufferLocked(elapsedRealtimeMs, uptimeMs, + reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME, mHistoryCur); mHistoryCur.currentTime = 0; if (reset) { @@ -6078,19 +6126,21 @@ public final class BatteryStatsImpl extends BatteryStats { addHistoryRecordLocked(elapsedRealtime, uptime); } if (onBattery) { - if (mLastDischargeStepLevel != level) { + if (mLastDischargeStepLevel != level && mMinDischargeStepLevel > level) { mNumDischargeStepDurations = addLevelSteps(mDischargeStepDurations, mNumDischargeStepDurations, mLastDischargeStepTime, mLastDischargeStepLevel - level, elapsedRealtime); mLastDischargeStepLevel = level; + mMinDischargeStepLevel = level; mLastDischargeStepTime = elapsedRealtime; } } else { - if (mLastChargeStepLevel != level) { + if (mLastChargeStepLevel != level && mMaxChargeStepLevel < level) { mNumChargeStepDurations = addLevelSteps(mChargeStepDurations, mNumChargeStepDurations, mLastChargeStepTime, level - mLastChargeStepLevel, elapsedRealtime); mLastChargeStepLevel = level; + mMaxChargeStepLevel = level; mLastChargeStepTime = elapsedRealtime; } } @@ -6939,6 +6989,7 @@ public final class BatteryStatsImpl extends BatteryStats { mInteractive = false; mInteractiveTimer.readSummaryFromParcelLocked(in); mPhoneOn = false; + mLowPowerModeEnabledTimer.readSummaryFromParcelLocked(in); mPhoneOnTimer.readSummaryFromParcelLocked(in); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i].readSummaryFromParcelLocked(in); @@ -7193,6 +7244,7 @@ public final class BatteryStatsImpl extends BatteryStats { mScreenBrightnessTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); } mInteractiveTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); + mLowPowerModeEnabledTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); mPhoneOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS); @@ -7454,8 +7506,11 @@ public final class BatteryStatsImpl extends BatteryStats { mScreenBrightnessTimer[i] = new StopwatchTimer(null, -100-i, null, mOnBatteryTimeBase, in); } + mInteractive = false; + mInteractiveTimer = new StopwatchTimer(null, -9, null, mOnBatteryTimeBase, in); mPhoneOn = false; - mPhoneOnTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); + mLowPowerModeEnabledTimer = new StopwatchTimer(null, -2, null, mOnBatteryTimeBase, in); + mPhoneOnTimer = new StopwatchTimer(null, -3, null, mOnBatteryTimeBase, in); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i] = new StopwatchTimer(null, -200-i, null, mOnBatteryTimeBase, in); @@ -7477,25 +7532,23 @@ public final class BatteryStatsImpl extends BatteryStats { mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase, in); mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase, in); mWifiOn = false; - mWifiOnTimer = new StopwatchTimer(null, -3, null, mOnBatteryTimeBase, in); + mWifiOnTimer = new StopwatchTimer(null, -4, null, mOnBatteryTimeBase, in); mGlobalWifiRunning = false; - mGlobalWifiRunningTimer = new StopwatchTimer(null, -4, null, mOnBatteryTimeBase, in); + mGlobalWifiRunningTimer = new StopwatchTimer(null, -5, 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, -5, null, mOnBatteryTimeBase, in); + mBluetoothOnTimer = new StopwatchTimer(null, -6, null, mOnBatteryTimeBase, in); for (int i=0; i< NUM_BLUETOOTH_STATES; i++) { mBluetoothStateTimer[i] = new StopwatchTimer(null, -500-i, null, mOnBatteryTimeBase, in); } mAudioOn = false; - mAudioOnTimer = new StopwatchTimer(null, -6, null, mOnBatteryTimeBase); + mAudioOnTimer = new StopwatchTimer(null, -7, null, mOnBatteryTimeBase); mVideoOn = false; - mVideoOnTimer = new StopwatchTimer(null, -7, null, mOnBatteryTimeBase); - mInteractive = false; - mInteractiveTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase, in); + mVideoOnTimer = new StopwatchTimer(null, -8, null, mOnBatteryTimeBase); mDischargeUnplugLevel = in.readInt(); mDischargePlugLevel = in.readInt(); mDischargeCurrentLevel = in.readInt(); @@ -7594,6 +7647,7 @@ public final class BatteryStatsImpl extends BatteryStats { mScreenBrightnessTimer[i].writeToParcel(out, uSecRealtime); } mInteractiveTimer.writeToParcel(out, uSecRealtime); + mLowPowerModeEnabledTimer.writeToParcel(out, uSecRealtime); mPhoneOnTimer.writeToParcel(out, uSecRealtime); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { mPhoneSignalStrengthsTimer[i].writeToParcel(out, uSecRealtime); @@ -7712,6 +7766,8 @@ public final class BatteryStatsImpl extends BatteryStats { } pr.println("*** Interactive timer:"); mInteractiveTimer.logState(pr, " "); + pr.println("*** Low power mode timer:"); + mLowPowerModeEnabledTimer.logState(pr, " "); pr.println("*** Phone timer:"); mPhoneOnTimer.logState(pr, " "); for (int i=0; i<SignalStrength.NUM_SIGNAL_STRENGTH_BINS; i++) { diff --git a/core/java/com/android/internal/os/HandlerCaller.java b/core/java/com/android/internal/os/HandlerCaller.java index 40834ba..17685fd 100644 --- a/core/java/com/android/internal/os/HandlerCaller.java +++ b/core/java/com/android/internal/os/HandlerCaller.java @@ -22,9 +22,6 @@ import android.os.Looper; import android.os.Message; public class HandlerCaller { - - public final Context mContext; - final Looper mMainLooper; final Handler mH; @@ -47,7 +44,6 @@ public class HandlerCaller { public HandlerCaller(Context context, Looper looper, Callback callback, boolean asyncHandler) { - mContext = context; mMainLooper = looper != null ? looper : context.getMainLooper(); mH = new MyHandler(mMainLooper, asyncHandler); mCallback = callback; diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 3ea749e..5ce658b 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -23,6 +23,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.net.LocalServerSocket; import android.opengl.EGL14; +import android.os.Build; import android.os.Debug; import android.os.Process; import android.os.SystemClock; @@ -505,7 +506,7 @@ public class ZygoteInit { /** * Prepare the arguments and fork for the system server process. */ - private static boolean startSystemServer() + private static boolean startSystemServer(String abiList, String socketName) throws MethodAndArgsCaller, RuntimeException { long capabilities = posixCapabilitiesAsBits( OsConstants.CAP_BLOCK_SUSPEND, @@ -553,6 +554,10 @@ public class ZygoteInit { /* For child process */ if (pid == 0) { + if (hasSecondZygote(abiList)) { + waitForSecondaryZygote(socketName); + } + handleSystemServerProcess(parsedArgs); } @@ -615,7 +620,7 @@ public class ZygoteInit { Trace.setTracingEnabled(false); if (startSystemServer) { - startSystemServer(); + startSystemServer(abiList, socketName); } Log.i(TAG, "Accepting command socket connections"); @@ -632,6 +637,36 @@ public class ZygoteInit { } /** + * Return {@code true} if this device configuration has another zygote. + * + * We determine this by comparing the device ABI list with this zygotes + * list. If this zygote supports all ABIs this device supports, there won't + * be another zygote. + */ + private static boolean hasSecondZygote(String abiList) { + return !SystemProperties.get("ro.product.cpu.abilist").equals(abiList); + } + + private static void waitForSecondaryZygote(String socketName) { + String otherZygoteName = Process.ZYGOTE_SOCKET.equals(socketName) ? + Process.SECONDARY_ZYGOTE_SOCKET : Process.ZYGOTE_SOCKET; + while (true) { + try { + final Process.ZygoteState zs = Process.ZygoteState.connect(otherZygoteName); + zs.close(); + break; + } catch (IOException ioe) { + Log.w(TAG, "Got error connecting to zygote, retrying. msg= " + ioe.getMessage()); + } + + try { + Thread.sleep(1000); + } catch (InterruptedException ie) { + } + } + } + + /** * Runs the zygote process's select loop. Accepts new connections as * they happen, and reads commands from connections one spawn-request's * worth at a time. diff --git a/core/java/com/android/internal/policy/IKeyguardService.aidl b/core/java/com/android/internal/policy/IKeyguardService.aidl index b78c70f..a5421f5 100644 --- a/core/java/com/android/internal/policy/IKeyguardService.aidl +++ b/core/java/com/android/internal/policy/IKeyguardService.aidl @@ -56,4 +56,13 @@ interface IKeyguardService { oneway void dispatch(in MotionEvent event); oneway void launchCamera(); oneway void onBootCompleted(); + + /** + * Notifies that the activity behind has now been drawn and it's safe to remove the wallpaper + * and keyguard flag. + * + * @param startTime the start time of the animation in uptime milliseconds + * @param fadeoutDuration the duration of the exit animation, in milliseconds + */ + oneway void startKeyguardExitAnimation(long startTime, long fadeoutDuration); } diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 75feb5d..a01e9b7 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -24,9 +24,9 @@ oneway interface IStatusBar { void setIcon(int index, in StatusBarIcon icon); void removeIcon(int index); - void addNotification(IBinder key, in StatusBarNotification notification); - void updateNotification(IBinder key, in StatusBarNotification notification); - void removeNotification(IBinder key); + void addNotification(in StatusBarNotification notification); + void updateNotification(in StatusBarNotification notification); + void removeNotification(String key); void disable(int state); void animateExpandNotificationsPanel(); void animateExpandSettingsPanel(); @@ -36,9 +36,12 @@ oneway interface IStatusBar void setImeWindowStatus(in IBinder token, int vis, int backDisposition, boolean showImeSwitcher); void setHardKeyboardStatus(boolean available, boolean enabled); + void setWindowState(int window, int state); + + void showRecentApps(boolean triggeredFromAltTab); + void hideRecentApps(boolean triggeredFromAltTab); void toggleRecentApps(); void preloadRecentApps(); void cancelPreloadRecentApps(); - void setWindowState(int window, int state); } diff --git a/core/java/com/android/internal/statusbar/IStatusBarService.aidl b/core/java/com/android/internal/statusbar/IStatusBarService.aidl index cf334c3..a3b417f 100644 --- a/core/java/com/android/internal/statusbar/IStatusBarService.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBarService.aidl @@ -39,8 +39,8 @@ interface IStatusBarService // ---- Methods below are for use by the status bar policy services ---- // You need the STATUS_BAR_SERVICE permission void registerStatusBar(IStatusBar callbacks, out StatusBarIconList iconList, - out List<IBinder> notificationKeys, out List<StatusBarNotification> notifications, - out int[] switches, out List<IBinder> binders); + out List<StatusBarNotification> notifications, out int[] switches, + out List<IBinder> binders); void onPanelRevealed(); void onPanelHidden(); void onNotificationClick(String key); @@ -52,8 +52,11 @@ interface IStatusBarService in String[] newlyVisibleKeys, in String[] noLongerVisibleKeys); void setSystemUiVisibility(int vis, int mask); void setHardKeyboardEnabled(boolean enabled); + void setWindowState(int window, int state); + + void showRecentApps(boolean triggeredFromAltTab); + void hideRecentApps(boolean triggeredFromAltTab); void toggleRecentApps(); void preloadRecentApps(); void cancelPreloadRecentApps(); - void setWindowState(int window, int state); } diff --git a/core/java/com/android/internal/util/ArrayUtils.java b/core/java/com/android/internal/util/ArrayUtils.java index d177410..d66ef83 100644 --- a/core/java/com/android/internal/util/ArrayUtils.java +++ b/core/java/com/android/internal/util/ArrayUtils.java @@ -117,6 +117,13 @@ public class ArrayUtils } /** + * Checks if given array is null or has zero elements. + */ + public static <T> boolean isEmpty(T[] array) { + return array == null || array.length == 0; + } + + /** * Checks that value is present as at least one of the elements of the array. * @param array the array to check in * @param value the value to check for @@ -162,6 +169,15 @@ public class ArrayUtils return false; } + public static boolean contains(long[] array, long value) { + for (long element : array) { + if (element == value) { + return true; + } + } + return false; + } + public static long total(long[] array) { long total = 0; for (long value : array) { @@ -222,6 +238,14 @@ public class ArrayUtils return array; } + /** + * Appends a new value to a copy of the array and returns the copy. If + * the value is already present, the original array is returned + * @param cur The original array, or null to represent an empty array. + * @param val The value to add. + * @return A new array that contains all of the values of the original array + * with the new value added, or the original array. + */ public static int[] appendInt(int[] cur, int val) { if (cur == null) { return new int[] { val }; @@ -257,4 +281,48 @@ public class ArrayUtils } return cur; } + + /** + * Appends a new value to a copy of the array and returns the copy. If + * the value is already present, the original array is returned + * @param cur The original array, or null to represent an empty array. + * @param val The value to add. + * @return A new array that contains all of the values of the original array + * with the new value added, or the original array. + */ + public static long[] appendLong(long[] cur, long val) { + if (cur == null) { + return new long[] { val }; + } + final int N = cur.length; + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + return cur; + } + } + long[] ret = new long[N + 1]; + System.arraycopy(cur, 0, ret, 0, N); + ret[N] = val; + return ret; + } + + public static long[] removeLong(long[] cur, long val) { + if (cur == null) { + return null; + } + final int N = cur.length; + for (int i = 0; i < N; i++) { + if (cur[i] == val) { + long[] ret = new long[N - 1]; + if (i > 0) { + System.arraycopy(cur, 0, ret, 0, i); + } + if (i < (N - 1)) { + System.arraycopy(cur, i + 1, ret, i, N - i - 1); + } + return ret; + } + } + return cur; + } } diff --git a/core/java/com/android/internal/util/AsyncChannel.java b/core/java/com/android/internal/util/AsyncChannel.java index 52281d9..34f62ba 100644 --- a/core/java/com/android/internal/util/AsyncChannel.java +++ b/core/java/com/android/internal/util/AsyncChannel.java @@ -450,6 +450,7 @@ public class AsyncChannel { public void disconnect() { if ((mConnection != null) && (mSrcContext != null)) { mSrcContext.unbindService(mConnection); + mConnection = null; } try { // Send the DISCONNECTED, although it may not be received @@ -463,10 +464,12 @@ public class AsyncChannel { // Tell source we're disconnected. if (mSrcHandler != null) { replyDisconnected(STATUS_SUCCESSFUL); + mSrcHandler = null; } // Unlink only when bindService isn't used if (mConnection == null && mDstMessenger != null && mDeathMonitor!= null) { mDstMessenger.getBinder().unlinkToDeath(mDeathMonitor, 0); + mDeathMonitor = null; } } diff --git a/core/java/com/android/internal/util/Preconditions.java b/core/java/com/android/internal/util/Preconditions.java index f6722a6..c0d1e88 100644 --- a/core/java/com/android/internal/util/Preconditions.java +++ b/core/java/com/android/internal/util/Preconditions.java @@ -183,6 +183,33 @@ public class Preconditions { } /** + * Ensures that the argument int value is within the inclusive range. + * + * @param value a int value + * @param lower the lower endpoint of the inclusive range + * @param upper the upper endpoint of the inclusive range + * @param valueName the name of the argument to use if the check fails + * + * @return the validated int value + * + * @throws IllegalArgumentException if {@code value} was not within the range + */ + public static int checkArgumentInRange(int value, int lower, int upper, + String valueName) { + if (value < lower) { + throw new IllegalArgumentException( + String.format( + "%s is out of range of [%d, %d] (too low)", valueName, lower, upper)); + } else if (value > upper) { + throw new IllegalArgumentException( + String.format( + "%s is out of range of [%d, %d] (too high)", valueName, lower, upper)); + } + + return value; + } + + /** * Ensures that the array is not {@code null}, and none if its elements are {@code null}. * * @param value an array of boxed objects diff --git a/core/java/com/android/internal/util/Protocol.java b/core/java/com/android/internal/util/Protocol.java index bc92c4a..af966b1 100644 --- a/core/java/com/android/internal/util/Protocol.java +++ b/core/java/com/android/internal/util/Protocol.java @@ -48,6 +48,8 @@ public class Protocol { public static final int BASE_WIFI_CONTROLLER = 0x00026000; public static final int BASE_WIFI_SCANNER = 0x00027000; public static final int BASE_WIFI_SCANNER_SERVICE = 0x00027100; + public static final int BASE_WIFI_PASSPOINT_MANAGER = 0x00028000; + public static final int BASE_WIFI_PASSPOINT_SERVICE = 0x00028100; public static final int BASE_DHCP = 0x00030000; public static final int BASE_DATA_CONNECTION = 0x00040000; public static final int BASE_DATA_CONNECTION_AC = 0x00041000; @@ -55,5 +57,9 @@ public class Protocol { public static final int BASE_DNS_PINGER = 0x00050000; public static final int BASE_NSD_MANAGER = 0x00060000; public static final int BASE_NETWORK_STATE_TRACKER = 0x00070000; + public static final int BASE_CONNECTIVITY_MANAGER = 0x00080000; + public static final int BASE_NETWORK_AGENT = 0x00081000; + public static final int BASE_NETWORK_MONITOR = 0x00082000; + public static final int BASE_NETWORK_FACTORY = 0x00083000; //TODO: define all used protocols } diff --git a/core/java/com/android/internal/util/VirtualRefBasePtr.java b/core/java/com/android/internal/util/VirtualRefBasePtr.java new file mode 100644 index 0000000..52306f1 --- /dev/null +++ b/core/java/com/android/internal/util/VirtualRefBasePtr.java @@ -0,0 +1,53 @@ +/* + * 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; + +/** + * Helper class that contains a strong reference to a VirtualRefBase native + * object. This will incStrong in the ctor, and decStrong in the finalizer + */ +public final class VirtualRefBasePtr { + private long mNativePtr; + + public VirtualRefBasePtr(long ptr) { + mNativePtr = ptr; + nIncStrong(mNativePtr); + } + + public long get() { + return mNativePtr; + } + + public void release() { + if (mNativePtr != 0) { + nDecStrong(mNativePtr); + mNativePtr = 0; + } + } + + @Override + protected void finalize() throws Throwable { + try { + release(); + } finally { + super.finalize(); + } + } + + private static native void nIncStrong(long ptr); + private static native void nDecStrong(long ptr); +} diff --git a/core/java/com/android/internal/util/XmlUtils.java b/core/java/com/android/internal/util/XmlUtils.java index b35de93..dca9921 100644 --- a/core/java/com/android/internal/util/XmlUtils.java +++ b/core/java/com/android/internal/util/XmlUtils.java @@ -220,28 +220,74 @@ public class XmlUtils { * @see #readMapXml */ public static final void writeMapXml(Map val, String name, XmlSerializer out) - throws XmlPullParserException, java.io.IOException - { + throws XmlPullParserException, java.io.IOException { + writeMapXml(val, name, out, null); + } + + /** + * Flatten a Map into an XmlSerializer. The map can later be read back + * with readThisMapXml(). + * + * @param val The map to be flattened. + * @param name Name attribute to include with this list's tag, or null for + * none. + * @param out XmlSerializer to write the map into. + * @param callback Method to call when an Object type is not recognized. + * + * @see #writeMapXml(Map, OutputStream) + * @see #writeListXml + * @see #writeValueXml + * @see #readMapXml + * + * @hide + */ + public static final void writeMapXml(Map val, String name, XmlSerializer out, + WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { + if (val == null) { out.startTag(null, "null"); out.endTag(null, "null"); return; } - Set s = val.entrySet(); - Iterator i = s.iterator(); - out.startTag(null, "map"); if (name != null) { out.attribute(null, "name", name); } + writeMapXml(val, out, callback); + + out.endTag(null, "map"); + } + + /** + * Flatten a Map into an XmlSerializer. The map can later be read back + * with readThisMapXml(). This method presumes that the start tag and + * name attribute have already been written and does not write an end tag. + * + * @param val The map to be flattened. + * @param out XmlSerializer to write the map into. + * + * @see #writeMapXml(Map, OutputStream) + * @see #writeListXml + * @see #writeValueXml + * @see #readMapXml + * + * @hide + */ + public static final void writeMapXml(Map val, XmlSerializer out, + WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { + if (val == null) { + return; + } + + Set s = val.entrySet(); + Iterator i = s.iterator(); + while (i.hasNext()) { Map.Entry e = (Map.Entry)i.next(); - writeValueXml(e.getValue(), (String)e.getKey(), out); + writeValueXml(e.getValue(), (String)e.getKey(), out, callback); } - - out.endTag(null, "map"); } /** @@ -387,6 +433,123 @@ public class XmlUtils { } /** + * Flatten a long[] into an XmlSerializer. The list can later be read back + * with readThisLongArrayXml(). + * + * @param val The long array to be flattened. + * @param name Name attribute to include with this array's tag, or null for + * none. + * @param out XmlSerializer to write the array into. + * + * @see #writeMapXml + * @see #writeValueXml + * @see #readThisIntArrayXml + */ + public static final void writeLongArrayXml(long[] val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "long-array"); + if (name != null) { + out.attribute(null, "name", name); + } + + final int N = val.length; + out.attribute(null, "num", Integer.toString(N)); + + for (int i=0; i<N; i++) { + out.startTag(null, "item"); + out.attribute(null, "value", Long.toString(val[i])); + out.endTag(null, "item"); + } + + out.endTag(null, "long-array"); + } + + /** + * Flatten a double[] into an XmlSerializer. The list can later be read back + * with readThisDoubleArrayXml(). + * + * @param val The double array to be flattened. + * @param name Name attribute to include with this array's tag, or null for + * none. + * @param out XmlSerializer to write the array into. + * + * @see #writeMapXml + * @see #writeValueXml + * @see #readThisIntArrayXml + */ + public static final void writeDoubleArrayXml(double[] val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "double-array"); + if (name != null) { + out.attribute(null, "name", name); + } + + final int N = val.length; + out.attribute(null, "num", Integer.toString(N)); + + for (int i=0; i<N; i++) { + out.startTag(null, "item"); + out.attribute(null, "value", Double.toString(val[i])); + out.endTag(null, "item"); + } + + out.endTag(null, "double-array"); + } + + /** + * Flatten a String[] into an XmlSerializer. The list can later be read back + * with readThisStringArrayXml(). + * + * @param val The long array to be flattened. + * @param name Name attribute to include with this array's tag, or null for + * none. + * @param out XmlSerializer to write the array into. + * + * @see #writeMapXml + * @see #writeValueXml + * @see #readThisIntArrayXml + */ + public static final void writeStringArrayXml(String[] val, String name, XmlSerializer out) + throws XmlPullParserException, java.io.IOException { + + if (val == null) { + out.startTag(null, "null"); + out.endTag(null, "null"); + return; + } + + out.startTag(null, "string-array"); + if (name != null) { + out.attribute(null, "name", name); + } + + final int N = val.length; + out.attribute(null, "num", Integer.toString(N)); + + for (int i=0; i<N; i++) { + out.startTag(null, "item"); + out.attribute(null, "value", val[i]); + out.endTag(null, "item"); + } + + out.endTag(null, "string-array"); + } + + /** * Flatten an object's value into an XmlSerializer. The value can later * be read back with readThisValueXml(). * @@ -403,8 +566,29 @@ public class XmlUtils { * @see #readValueXml */ public static final void writeValueXml(Object v, String name, XmlSerializer out) - throws XmlPullParserException, java.io.IOException - { + throws XmlPullParserException, java.io.IOException { + writeValueXml(v, name, out, null); + } + + /** + * Flatten an object's value into an XmlSerializer. The value can later + * be read back with readThisValueXml(). + * + * Currently supported value types are: null, String, Integer, Long, + * Float, Double Boolean, Map, List. + * + * @param v The object to be flattened. + * @param name Name attribute to include with this value's tag, or null + * for none. + * @param out XmlSerializer to write the object into. + * @param callback Handler for Object types not recognized. + * + * @see #writeMapXml + * @see #writeListXml + * @see #readValueXml + */ + private static final void writeValueXml(Object v, String name, XmlSerializer out, + WriteMapCallback callback) throws XmlPullParserException, java.io.IOException { String typeStr; if (v == null) { out.startTag(null, "null"); @@ -437,14 +621,23 @@ public class XmlUtils { } else if (v instanceof int[]) { writeIntArrayXml((int[])v, name, out); return; + } else if (v instanceof long[]) { + writeLongArrayXml((long[])v, name, out); + return; + } else if (v instanceof double[]) { + writeDoubleArrayXml((double[])v, name, out); + return; + } else if (v instanceof String[]) { + writeStringArrayXml((String[])v, name, out); + return; } else if (v instanceof Map) { writeMapXml((Map)v, name, out); return; } else if (v instanceof List) { - writeListXml((List)v, name, out); + writeListXml((List) v, name, out); return; } else if (v instanceof Set) { - writeSetXml((Set)v, name, out); + writeSetXml((Set) v, name, out); return; } else if (v instanceof CharSequence) { // XXX This is to allow us to at least write something if @@ -457,6 +650,9 @@ public class XmlUtils { out.text(v.toString()); out.endTag(null, "string"); return; + } else if (callback != null) { + callback.writeUnknownObject(v, name, out); + return; } else { throw new RuntimeException("writeValueXml: unable to write value " + v); } @@ -550,14 +746,35 @@ public class XmlUtils { * @see #readMapXml */ public static final HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag, - String[] name) throws XmlPullParserException, java.io.IOException + String[] name) throws XmlPullParserException, java.io.IOException { + return readThisMapXml(parser, endTag, name, null); + } + + /** + * Read a HashMap object from an XmlPullParser. The XML data could + * previously have been generated by writeMapXml(). The XmlPullParser + * must be positioned <em>after</em> the tag that begins the map. + * + * @param parser The XmlPullParser from which to read the map data. + * @param endTag Name of the tag that will end the map, usually "map". + * @param name An array of one string, used to return the name attribute + * of the map's tag. + * + * @return HashMap The newly generated map. + * + * @see #readMapXml + * @hide + */ + public static final HashMap<String, ?> readThisMapXml(XmlPullParser parser, String endTag, + String[] name, ReadMapCallback callback) + throws XmlPullParserException, java.io.IOException { HashMap<String, Object> map = new HashMap<String, Object>(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name); + Object val = readThisValueXml(parser, name, callback); map.put(name[0], val); } else if (eventType == parser.END_TAG) { if (parser.getName().equals(endTag)) { @@ -587,15 +804,34 @@ public class XmlUtils { * * @see #readListXml */ - public static final ArrayList readThisListXml(XmlPullParser parser, String endTag, String[] name) - throws XmlPullParserException, java.io.IOException - { + public static final ArrayList readThisListXml(XmlPullParser parser, String endTag, + String[] name) throws XmlPullParserException, java.io.IOException { + return readThisListXml(parser, endTag, name, null); + } + + /** + * Read an ArrayList object from an XmlPullParser. The XML data could + * previously have been generated by writeListXml(). The XmlPullParser + * must be positioned <em>after</em> the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "list". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * + * @return HashMap The newly generated list. + * + * @see #readListXml + */ + private static final ArrayList readThisListXml(XmlPullParser parser, String endTag, + String[] name, ReadMapCallback callback) + throws XmlPullParserException, java.io.IOException { ArrayList list = new ArrayList(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name); + Object val = readThisValueXml(parser, name, callback); list.add(val); //System.out.println("Adding to list: " + val); } else if (eventType == parser.END_TAG) { @@ -611,7 +847,29 @@ public class XmlUtils { throw new XmlPullParserException( "Document ended before " + endTag + " end tag"); } - + + /** + * Read a HashSet object from an XmlPullParser. The XML data could previously + * have been generated by writeSetXml(). The XmlPullParser must be positioned + * <em>after</em> the tag that begins the set. + * + * @param parser The XmlPullParser from which to read the set data. + * @param endTag Name of the tag that will end the set, usually "set". + * @param name An array of one string, used to return the name attribute + * of the set's tag. + * + * @return HashSet The newly generated set. + * + * @throws XmlPullParserException + * @throws java.io.IOException + * + * @see #readSetXml + */ + public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + return readThisSetXml(parser, endTag, name, null); + } + /** * Read a HashSet object from an XmlPullParser. The XML data could previously * have been generated by writeSetXml(). The XmlPullParser must be positioned @@ -628,15 +886,16 @@ public class XmlUtils { * @throws java.io.IOException * * @see #readSetXml + * @hide */ - public static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name) - throws XmlPullParserException, java.io.IOException { + private static final HashSet readThisSetXml(XmlPullParser parser, String endTag, String[] name, + ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { HashSet set = new HashSet(); int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - Object val = readThisValueXml(parser, name); + Object val = readThisValueXml(parser, name, callback); set.add(val); //System.out.println("Adding to set: " + val); } else if (eventType == parser.END_TAG) { @@ -681,6 +940,7 @@ public class XmlUtils { throw new XmlPullParserException( "Not a number in num attribute in byte-array"); } + parser.next(); int[] array = new int[num]; int i = 0; @@ -722,6 +982,187 @@ public class XmlUtils { } /** + * Read a long[] object from an XmlPullParser. The XML data could + * previously have been generated by writeLongArrayXml(). The XmlPullParser + * must be positioned <em>after</em> the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "list". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * + * @return Returns a newly generated long[]. + * + * @see #readListXml + */ + public static final long[] readThisLongArrayXml(XmlPullParser parser, + String endTag, String[] name) + throws XmlPullParserException, java.io.IOException { + + int num; + try { + num = Integer.parseInt(parser.getAttributeValue(null, "num")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need num attribute in long-array"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in num attribute in long-array"); + } + parser.next(); + + long[] array = new long[num]; + int i = 0; + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + if (parser.getName().equals("item")) { + try { + array[i] = Long.parseLong(parser.getAttributeValue(null, "value")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need value attribute in item"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in value attribute in item"); + } + } else { + throw new XmlPullParserException("Expected item tag at: " + parser.getName()); + } + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return array; + } else if (parser.getName().equals("item")) { + i++; + } else { + throw new XmlPullParserException("Expected " + endTag + " end tag at: " + + parser.getName()); + } + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException("Document ended before " + endTag + " end tag"); + } + + /** + * Read a double[] object from an XmlPullParser. The XML data could + * previously have been generated by writeDoubleArrayXml(). The XmlPullParser + * must be positioned <em>after</em> the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "double-array". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * + * @return Returns a newly generated double[]. + * + * @see #readListXml + */ + public static final double[] readThisDoubleArrayXml(XmlPullParser parser, String endTag, + String[] name) throws XmlPullParserException, java.io.IOException { + + int num; + try { + num = Integer.parseInt(parser.getAttributeValue(null, "num")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need num attribute in double-array"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in num attribute in double-array"); + } + parser.next(); + + double[] array = new double[num]; + int i = 0; + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + if (parser.getName().equals("item")) { + try { + array[i] = Double.parseDouble(parser.getAttributeValue(null, "value")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need value attribute in item"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in value attribute in item"); + } + } else { + throw new XmlPullParserException("Expected item tag at: " + parser.getName()); + } + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return array; + } else if (parser.getName().equals("item")) { + i++; + } else { + throw new XmlPullParserException("Expected " + endTag + " end tag at: " + + parser.getName()); + } + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException("Document ended before " + endTag + " end tag"); + } + + /** + * Read a String[] object from an XmlPullParser. The XML data could + * previously have been generated by writeStringArrayXml(). The XmlPullParser + * must be positioned <em>after</em> the tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "string-array". + * @param name An array of one string, used to return the name attribute + * of the list's tag. + * + * @return Returns a newly generated String[]. + * + * @see #readListXml + */ + public static final String[] readThisStringArrayXml(XmlPullParser parser, String endTag, + String[] name) throws XmlPullParserException, java.io.IOException { + + int num; + try { + num = Integer.parseInt(parser.getAttributeValue(null, "num")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need num attribute in string-array"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in num attribute in string-array"); + } + parser.next(); + + String[] array = new String[num]; + int i = 0; + + int eventType = parser.getEventType(); + do { + if (eventType == parser.START_TAG) { + if (parser.getName().equals("item")) { + try { + array[i] = parser.getAttributeValue(null, "value"); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need value attribute in item"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in value attribute in item"); + } + } else { + throw new XmlPullParserException("Expected item tag at: " + parser.getName()); + } + } else if (eventType == parser.END_TAG) { + if (parser.getName().equals(endTag)) { + return array; + } else if (parser.getName().equals("item")) { + i++; + } else { + throw new XmlPullParserException("Expected " + endTag + " end tag at: " + + parser.getName()); + } + } + eventType = parser.next(); + } while (eventType != parser.END_DOCUMENT); + + throw new XmlPullParserException("Document ended before " + endTag + " end tag"); + } + + /** * Read a flattened object from an XmlPullParser. The XML data could * previously have been written with writeMapXml(), writeListXml(), or * writeValueXml(). The XmlPullParser must be positioned <em>at</em> the @@ -743,7 +1184,7 @@ public class XmlUtils { int eventType = parser.getEventType(); do { if (eventType == parser.START_TAG) { - return readThisValueXml(parser, name); + return readThisValueXml(parser, name, null); } else if (eventType == parser.END_TAG) { throw new XmlPullParserException( "Unexpected end tag at: " + parser.getName()); @@ -758,9 +1199,8 @@ public class XmlUtils { "Unexpected end of document"); } - private static final Object readThisValueXml(XmlPullParser parser, String[] name) - throws XmlPullParserException, java.io.IOException - { + private static final Object readThisValueXml(XmlPullParser parser, String[] name, + ReadMapCallback callback) throws XmlPullParserException, java.io.IOException { final String valueName = parser.getAttributeValue(null, "name"); final String tagName = parser.getName(); @@ -794,11 +1234,25 @@ public class XmlUtils { } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) { // all work already done by readThisPrimitiveValueXml } else if (tagName.equals("int-array")) { - parser.next(); res = readThisIntArrayXml(parser, "int-array", name); name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; + } else if (tagName.equals("long-array")) { + res = readThisLongArrayXml(parser, "long-array", name); + name[0] = valueName; + //System.out.println("Returning value for " + valueName + ": " + res); + return res; + } else if (tagName.equals("double-array")) { + res = readThisDoubleArrayXml(parser, "double-array", name); + name[0] = valueName; + //System.out.println("Returning value for " + valueName + ": " + res); + return res; + } else if (tagName.equals("string-array")) { + res = readThisStringArrayXml(parser, "string-array", name); + name[0] = valueName; + //System.out.println("Returning value for " + valueName + ": " + res); + return res; } else if (tagName.equals("map")) { parser.next(); res = readThisMapXml(parser, "map", name); @@ -817,9 +1271,12 @@ public class XmlUtils { name[0] = valueName; //System.out.println("Returning value for " + valueName + ": " + res); return res; + } else if (callback != null) { + res = callback.readThisUnknownObjectXml(parser, tagName); + name[0] = valueName; + return res; } else { - throw new XmlPullParserException( - "Unknown tag: " + tagName); + throw new XmlPullParserException("Unknown tag: " + tagName); } // Skip through to end tag. @@ -912,6 +1369,15 @@ public class XmlUtils { } } + public static int readIntAttribute(XmlPullParser in, String name, int defaultValue) { + final String value = in.getAttributeValue(null, name); + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } + public static int readIntAttribute(XmlPullParser in, String name) throws IOException { final String value = in.getAttributeValue(null, name); try { @@ -958,4 +1424,39 @@ public class XmlUtils { throws IOException { out.attribute(null, name, Boolean.toString(value)); } + + /** @hide */ + public interface WriteMapCallback { + /** + * Called from writeMapXml when an Object type is not recognized. The implementer + * must write out the entire element including start and end tags. + * + * @param v The object to be written out + * @param name The mapping key for v. Must be written into the "name" attribute of the + * start tag. + * @param out The XML output stream. + * @throws XmlPullParserException on unrecognized Object type. + * @throws IOException on XmlSerializer serialization errors. + * @hide + */ + public void writeUnknownObject(Object v, String name, XmlSerializer out) + throws XmlPullParserException, IOException; + } + + /** @hide */ + public interface ReadMapCallback { + /** + * Called from readThisMapXml when a START_TAG is not recognized. The input stream + * is positioned within the start tag so that attributes can be read using in.getAttribute. + * + * @param in the XML input stream + * @param tag the START_TAG that was not recognized. + * @return the Object parsed from the stream which will be put into the map. + * @throws XmlPullParserException if the START_TAG is not recognized. + * @throws IOException on XmlPullParser serialization errors. + * @hide + */ + public Object readThisUnknownObjectXml(XmlPullParser in, String tag) + throws XmlPullParserException, IOException; + } } diff --git a/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java new file mode 100644 index 0000000..06838c9 --- /dev/null +++ b/core/java/com/android/internal/view/animation/FallbackLUTInterpolator.java @@ -0,0 +1,75 @@ +/* + * 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.view.animation; + +import android.animation.TimeInterpolator; +import android.util.TimeUtils; +import android.view.Choreographer; + +/** + * Interpolator that builds a lookup table to use. This is a fallback for + * building a native interpolator from a TimeInterpolator that is not marked + * with {@link HasNativeInterpolator} + * + * This implements TimeInterpolator to allow for easier interop with Animators + */ +@HasNativeInterpolator +public class FallbackLUTInterpolator implements NativeInterpolatorFactory, TimeInterpolator { + + private TimeInterpolator mSourceInterpolator; + private final float mLut[]; + + /** + * Used to cache the float[] LUT for use across multiple native + * interpolator creation + */ + public FallbackLUTInterpolator(TimeInterpolator interpolator, long duration) { + mSourceInterpolator = interpolator; + mLut = createLUT(interpolator, duration); + } + + private static float[] createLUT(TimeInterpolator interpolator, long duration) { + long frameIntervalNanos = Choreographer.getInstance().getFrameIntervalNanos(); + int animIntervalMs = (int) (frameIntervalNanos / TimeUtils.NANOS_PER_MS); + int numAnimFrames = (int) Math.ceil(duration / animIntervalMs); + float values[] = new float[numAnimFrames]; + float lastFrame = numAnimFrames - 1; + for (int i = 0; i < numAnimFrames; i++) { + float inValue = i / lastFrame; + values[i] = interpolator.getInterpolation(inValue); + } + return values; + } + + @Override + public long createNativeInterpolator() { + return NativeInterpolatorFactoryHelper.createLutInterpolator(mLut); + } + + /** + * Used to create a one-shot float[] LUT & native interpolator + */ + public static long createNativeInterpolator(TimeInterpolator interpolator, long duration) { + float[] lut = createLUT(interpolator, duration); + return NativeInterpolatorFactoryHelper.createLutInterpolator(lut); + } + + @Override + public float getInterpolation(float input) { + return mSourceInterpolator.getInterpolation(input); + } +} diff --git a/core/java/com/android/internal/view/animation/HasNativeInterpolator.java b/core/java/com/android/internal/view/animation/HasNativeInterpolator.java new file mode 100644 index 0000000..48ea4da --- /dev/null +++ b/core/java/com/android/internal/view/animation/HasNativeInterpolator.java @@ -0,0 +1,37 @@ +/* + * 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.view.animation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This is a class annotation that signals that it is safe to create + * a native interpolator counterpart via {@link NativeInterpolatorFactory} + * + * The idea here is to prevent subclasses of interpolators from being treated as a + * NativeInterpolatorFactory, and instead have them fall back to the LUT & LERP + * method like a custom interpolator. + * + * @hide + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface HasNativeInterpolator { +} diff --git a/core/java/com/android/internal/view/animation/NativeInterpolatorFactory.java b/core/java/com/android/internal/view/animation/NativeInterpolatorFactory.java new file mode 100644 index 0000000..fcacd52 --- /dev/null +++ b/core/java/com/android/internal/view/animation/NativeInterpolatorFactory.java @@ -0,0 +1,21 @@ +/* + * 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.view.animation; + +public interface NativeInterpolatorFactory { + long createNativeInterpolator(); +} diff --git a/core/java/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java b/core/java/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java new file mode 100644 index 0000000..7cd75f3 --- /dev/null +++ b/core/java/com/android/internal/view/animation/NativeInterpolatorFactoryHelper.java @@ -0,0 +1,36 @@ +/* + * 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.view.animation; + +/** + * Static utility class for constructing native interpolators to keep the + * JNI simpler + */ +public final class NativeInterpolatorFactoryHelper { + private NativeInterpolatorFactoryHelper() {} + + public static native long createAccelerateDecelerateInterpolator(); + public static native long createAccelerateInterpolator(float factor); + public static native long createAnticipateInterpolator(float tension); + public static native long createAnticipateOvershootInterpolator(float tension); + public static native long createBounceInterpolator(); + public static native long createCycleInterpolator(float cycles); + public static native long createDecelerateInterpolator(float factor); + public static native long createLinearInterpolator(); + public static native long createOvershootInterpolator(float tension); + public static native long createLutInterpolator(float[] values); +} diff --git a/core/java/com/android/internal/widget/AbsActionBarView.java b/core/java/com/android/internal/widget/AbsActionBarView.java index 183478f..9e7ff93 100644 --- a/core/java/com/android/internal/widget/AbsActionBarView.java +++ b/core/java/com/android/internal/widget/AbsActionBarView.java @@ -34,7 +34,7 @@ import android.view.animation.DecelerateInterpolator; public abstract class AbsActionBarView extends ViewGroup { protected ActionMenuView mMenuView; protected ActionMenuPresenter mActionMenuPresenter; - protected ActionBarContainer mSplitView; + protected ViewGroup mSplitView; protected boolean mSplitActionBar; protected boolean mSplitWhenNarrow; protected int mContentHeight; @@ -74,7 +74,7 @@ public abstract class AbsActionBarView extends ViewGroup { setContentHeight(a.getLayoutDimension(R.styleable.ActionBar_height, 0)); a.recycle(); if (mSplitWhenNarrow) { - setSplitActionBar(getContext().getResources().getBoolean( + setSplitToolbar(getContext().getResources().getBoolean( com.android.internal.R.bool.split_action_bar_is_narrow)); } if (mActionMenuPresenter != null) { @@ -86,7 +86,7 @@ public abstract class AbsActionBarView extends ViewGroup { * Sets whether the bar should be split right now, no questions asked. * @param split true if the bar should split */ - public void setSplitActionBar(boolean split) { + public void setSplitToolbar(boolean split) { mSplitActionBar = split; } @@ -107,7 +107,7 @@ public abstract class AbsActionBarView extends ViewGroup { return mContentHeight; } - public void setSplitView(ActionBarContainer splitView) { + public void setSplitView(ViewGroup splitView) { mSplitView = splitView; } @@ -214,6 +214,10 @@ public abstract class AbsActionBarView extends ViewGroup { return mActionMenuPresenter != null && mActionMenuPresenter.isOverflowReserved(); } + public boolean canShowOverflowMenu() { + return isOverflowReserved() && getVisibility() == VISIBLE; + } + public void dismissPopupMenus() { if (mActionMenuPresenter != null) { mActionMenuPresenter.dismissPopupMenus(); diff --git a/core/java/com/android/internal/widget/ActionBarContainer.java b/core/java/com/android/internal/widget/ActionBarContainer.java index ed07514..790b611 100644 --- a/core/java/com/android/internal/widget/ActionBarContainer.java +++ b/core/java/com/android/internal/widget/ActionBarContainer.java @@ -36,7 +36,7 @@ import android.widget.FrameLayout; public class ActionBarContainer extends FrameLayout { private boolean mIsTransitioning; private View mTabContainer; - private ActionBarView mActionBarView; + private View mActionBarView; private Drawable mBackground; private Drawable mStackedBackground; @@ -76,7 +76,7 @@ public class ActionBarContainer extends FrameLayout { @Override public void onFinishInflate() { super.onFinishInflate(); - mActionBarView = (ActionBarView) findViewById(com.android.internal.R.id.action_bar); + mActionBarView = findViewById(com.android.internal.R.id.action_bar); } public void setPrimaryBackground(Drawable bg) { @@ -251,6 +251,10 @@ public class ActionBarContainer extends FrameLayout { return null; } + private boolean isCollapsed(View view) { + return view == null || view.getVisibility() == GONE || view.getMeasuredHeight() == 0; + } + @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mActionBarView == null && @@ -263,7 +267,7 @@ public class ActionBarContainer extends FrameLayout { if (mActionBarView == null) return; final LayoutParams lp = (LayoutParams) mActionBarView.getLayoutParams(); - final int actionBarViewHeight = mActionBarView.isCollapsed() ? 0 : + final int actionBarViewHeight = isCollapsed(mActionBarView) ? 0 : mActionBarView.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; if (mTabContainer != null && mTabContainer.getVisibility() != GONE) { @@ -298,9 +302,8 @@ public class ActionBarContainer extends FrameLayout { } } else { if (mBackground != null) { - final ActionBarView actionBarView = mActionBarView; - mBackground.setBounds(actionBarView.getLeft(), actionBarView.getTop(), - actionBarView.getRight(), actionBarView.getBottom()); + mBackground.setBounds(mActionBarView.getLeft(), mActionBarView.getTop(), + mActionBarView.getRight(), mActionBarView.getBottom()); needsInvalidate = true; } mIsStacked = hasTabs; diff --git a/core/java/com/android/internal/widget/ActionBarContextView.java b/core/java/com/android/internal/widget/ActionBarContextView.java index e10070f..6ff77a0 100644 --- a/core/java/com/android/internal/widget/ActionBarContextView.java +++ b/core/java/com/android/internal/widget/ActionBarContextView.java @@ -83,7 +83,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.ActionMode, defStyleAttr, defStyleRes); - setBackgroundDrawable(a.getDrawable( + setBackground(a.getDrawable( com.android.internal.R.styleable.ActionMode_background)); mTitleStyleRes = a.getResourceId( com.android.internal.R.styleable.ActionMode_titleTextStyle, 0); @@ -109,7 +109,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi } @Override - public void setSplitActionBar(boolean split) { + public void setSplitToolbar(boolean split) { if (mSplitActionBar != split) { if (mActionMenuPresenter != null) { // Mode is already active; move everything over and adjust the menu itself. @@ -137,7 +137,7 @@ public class ActionBarContextView extends AbsActionBarView implements AnimatorLi mSplitView.addView(mMenuView, layoutParams); } } - super.setSplitActionBar(split); + super.setSplitToolbar(split); } } diff --git a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java index 19d58bf..8a9cb22 100644 --- a/core/java/com/android/internal/widget/ActionBarOverlayLayout.java +++ b/core/java/com/android/internal/widget/ActionBarOverlayLayout.java @@ -19,26 +19,35 @@ package com.android.internal.widget; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; +import android.content.pm.ActivityInfo; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.Parcelable; import android.util.AttributeSet; import android.util.IntProperty; +import android.util.Log; import android.util.Property; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.view.ViewPropertyAnimator; +import android.view.Window; import android.view.WindowInsets; import android.widget.OverScroller; +import android.widget.Toolbar; +import com.android.internal.view.menu.MenuPresenter; /** * Special layout for the containing of an overlay action bar (and its * content) to correctly handle fitting system windows when the content * has request that its layout ignore them. */ -public class ActionBarOverlayLayout extends ViewGroup { +public class ActionBarOverlayLayout extends ViewGroup implements DecorContentParent { private static final String TAG = "ActionBarOverlayLayout"; private int mActionBarHeight; @@ -47,11 +56,11 @@ public class ActionBarOverlayLayout extends ViewGroup { // The main UI elements that we handle the layout of. private View mContent; - private View mActionBarBottom; + private ActionBarContainer mActionBarBottom; private ActionBarContainer mActionBarTop; // Some interior UI elements. - private ActionBarView mActionBarView; + private DecorToolbar mDecorToolbar; // Content overlay drawable - generally the action bar's shadow private Drawable mWindowContentOverlay; @@ -393,7 +402,7 @@ public class ActionBarOverlayLayout extends ViewGroup { topInset = mActionBarTop.getMeasuredHeight(); } - if (mActionBarView.isSplitActionBar()) { + if (mDecorToolbar.isSplit()) { // If action bar is split, adjust bottom insets for it. if (mActionBarBottom != null) { if (stable) { @@ -555,8 +564,20 @@ public class ActionBarOverlayLayout extends ViewGroup { mContent = findViewById(com.android.internal.R.id.content); 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); + mDecorToolbar = getDecorToolbar(findViewById(com.android.internal.R.id.action_bar)); + mActionBarBottom = (ActionBarContainer) findViewById( + com.android.internal.R.id.split_action_bar); + } + } + + private DecorToolbar getDecorToolbar(View view) { + if (view instanceof DecorToolbar) { + return (DecorToolbar) view; + } else if (view instanceof Toolbar) { + return ((Toolbar) view).getWrapper(); + } else { + throw new IllegalStateException("Can't make a decor toolbar out of " + + view.getClass().getSimpleName()); } } @@ -629,6 +650,179 @@ public class ActionBarOverlayLayout extends ViewGroup { return finalY > mActionBarTop.getHeight(); } + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + if (super.dispatchKeyEvent(event)) { + return true; + } + + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + final int action = event.getAction(); + + // Collapse any expanded action views. + if (mDecorToolbar != null && mDecorToolbar.hasExpandedActionView()) { + if (action == KeyEvent.ACTION_UP) { + mDecorToolbar.collapseActionView(); + } + return true; + } + } + + return false; + } + + @Override + public void setWindowCallback(Window.Callback cb) { + pullChildren(); + mDecorToolbar.setWindowCallback(cb); + } + + @Override + public void setWindowTitle(CharSequence title) { + pullChildren(); + mDecorToolbar.setWindowTitle(title); + } + + @Override + public CharSequence getTitle() { + pullChildren(); + return mDecorToolbar.getTitle(); + } + + @Override + public void initFeature(int windowFeature) { + pullChildren(); + switch (windowFeature) { + case Window.FEATURE_PROGRESS: + mDecorToolbar.initProgress(); + break; + case Window.FEATURE_INDETERMINATE_PROGRESS: + mDecorToolbar.initIndeterminateProgress(); + break; + case Window.FEATURE_ACTION_BAR_OVERLAY: + setOverlayMode(true); + break; + } + } + + @Override + public void setUiOptions(int uiOptions) { + boolean splitActionBar = false; + final boolean splitWhenNarrow = + (uiOptions & ActivityInfo.UIOPTION_SPLIT_ACTION_BAR_WHEN_NARROW) != 0; + if (splitWhenNarrow) { + splitActionBar = getContext().getResources().getBoolean( + com.android.internal.R.bool.split_action_bar_is_narrow); + } + if (splitActionBar) { + pullChildren(); + if (mActionBarBottom != null && mDecorToolbar.canSplit()) { + mDecorToolbar.setSplitView(mActionBarBottom); + mDecorToolbar.setSplitToolbar(splitActionBar); + mDecorToolbar.setSplitWhenNarrow(splitWhenNarrow); + + final ActionBarContextView cab = (ActionBarContextView) findViewById( + com.android.internal.R.id.action_context_bar); + cab.setSplitView(mActionBarBottom); + cab.setSplitToolbar(splitActionBar); + cab.setSplitWhenNarrow(splitWhenNarrow); + } else if (splitActionBar) { + Log.e(TAG, "Requested split action bar with " + + "incompatible window decor! Ignoring request."); + } + } + } + + @Override + public boolean hasIcon() { + pullChildren(); + return mDecorToolbar.hasIcon(); + } + + @Override + public boolean hasLogo() { + pullChildren(); + return mDecorToolbar.hasLogo(); + } + + @Override + public void setIcon(int resId) { + pullChildren(); + mDecorToolbar.setIcon(resId); + } + + @Override + public void setIcon(Drawable d) { + pullChildren(); + mDecorToolbar.setIcon(d); + } + + @Override + public void setLogo(int resId) { + pullChildren(); + mDecorToolbar.setLogo(resId); + } + + @Override + public boolean canShowOverflowMenu() { + pullChildren(); + return mDecorToolbar.canShowOverflowMenu(); + } + + @Override + public boolean isOverflowMenuShowing() { + pullChildren(); + return mDecorToolbar.isOverflowMenuShowing(); + } + + @Override + public boolean isOverflowMenuShowPending() { + pullChildren(); + return mDecorToolbar.isOverflowMenuShowPending(); + } + + @Override + public boolean showOverflowMenu() { + pullChildren(); + return mDecorToolbar.showOverflowMenu(); + } + + @Override + public boolean hideOverflowMenu() { + pullChildren(); + return mDecorToolbar.hideOverflowMenu(); + } + + @Override + public void setMenuPrepared() { + pullChildren(); + mDecorToolbar.setMenuPrepared(); + } + + @Override + public void setMenu(Menu menu, MenuPresenter.Callback cb) { + pullChildren(); + mDecorToolbar.setMenu(menu, cb); + } + + @Override + public void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) { + pullChildren(); + mDecorToolbar.saveHierarchyState(toolbarStates); + } + + @Override + public void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates) { + pullChildren(); + mDecorToolbar.restoreHierarchyState(toolbarStates); + } + + @Override + public void dismissPopups() { + pullChildren(); + mDecorToolbar.dismissPopupMenus(); + } + public static class LayoutParams extends MarginLayoutParams { public LayoutParams(Context c, AttributeSet attrs) { super(c, attrs); diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java index 60631b9..af82778 100644 --- a/core/java/com/android/internal/widget/ActionBarView.java +++ b/core/java/com/android/internal/widget/ActionBarView.java @@ -18,7 +18,6 @@ package com.android.internal.widget; import android.animation.LayoutTransition; import android.app.ActionBar; -import android.app.ActionBar.OnNavigationListener; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; @@ -63,7 +62,7 @@ import com.android.internal.view.menu.SubMenuBuilder; /** * @hide */ -public class ActionBarView extends AbsActionBarView { +public class ActionBarView extends AbsActionBarView implements DecorToolbar { private static final String TAG = "ActionBarView"; /** @@ -117,8 +116,7 @@ public class ActionBarView extends AbsActionBarView { private boolean mUserTitle; private boolean mIncludeTabs; - private boolean mIsCollapsable; - private boolean mIsCollapsed; + private boolean mIsCollapsible; private boolean mWasHomeEnabled; // Was it enabled before action view expansion? private MenuBuilder mOptionsMenu; @@ -129,7 +127,7 @@ public class ActionBarView extends AbsActionBarView { private ActionMenuItem mLogoNavItem; private SpinnerAdapter mSpinnerAdapter; - private OnNavigationListener mCallback; + private AdapterView.OnItemSelectedListener mNavItemSelectedListener; private Runnable mTabSelector; @@ -138,18 +136,6 @@ public class ActionBarView extends AbsActionBarView { Window.Callback mWindowCallback; - private final AdapterView.OnItemSelectedListener mNavItemSelectedListener = - new AdapterView.OnItemSelectedListener() { - public void onItemSelected(AdapterView parent, View view, int position, long id) { - if (mCallback != null) { - mCallback.onNavigationItemSelected(position, id); - } - } - public void onNothingSelected(AdapterView parent) { - // Do nothing - } - }; - private final OnClickListener mExpandedActionViewUpListener = new OnClickListener() { @Override public void onClick(View v) { @@ -178,8 +164,6 @@ public class ActionBarView extends AbsActionBarView { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ActionBar, com.android.internal.R.attr.actionBarStyle, 0); - ApplicationInfo appInfo = context.getApplicationInfo(); - PackageManager pm = context.getPackageManager(); mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode, ActionBar.NAVIGATION_MODE_STANDARD); mTitle = a.getText(R.styleable.ActionBar_title); @@ -260,7 +244,7 @@ public class ActionBarView extends AbsActionBarView { } if (mHomeDescriptionRes != 0) { - setHomeActionContentDescription(mHomeDescriptionRes); + setNavigationContentDescription(mHomeDescriptionRes); } if (mTabScrollView != null && mIncludeTabs) { @@ -313,7 +297,7 @@ public class ActionBarView extends AbsActionBarView { } @Override - public void setSplitActionBar(boolean splitActionBar) { + public void setSplitToolbar(boolean splitActionBar) { if (mSplitActionBar != splitActionBar) { if (mMenuView != null) { final ViewGroup oldParent = (ViewGroup) mMenuView.getParent(); @@ -349,18 +333,26 @@ public class ActionBarView extends AbsActionBarView { mActionMenuPresenter.setItemLimit(Integer.MAX_VALUE); } } - super.setSplitActionBar(splitActionBar); + super.setSplitToolbar(splitActionBar); } } - public boolean isSplitActionBar() { + public boolean isSplit() { return mSplitActionBar; } + public boolean canSplit() { + return true; + } + public boolean hasEmbeddedTabs() { return mIncludeTabs; } + public void setEmbeddedTabView(View view) { + setEmbeddedTabView((ScrollingTabContainerView) view); + } + public void setEmbeddedTabView(ScrollingTabContainerView tabs) { if (mTabScrollView != null) { removeView(mTabScrollView); @@ -376,10 +368,6 @@ public class ActionBarView extends AbsActionBarView { } } - public void setCallback(OnNavigationListener callback) { - mCallback = callback; - } - public void setMenuPrepared() { mMenuPrepared = true; } @@ -473,7 +461,7 @@ public class ActionBarView extends AbsActionBarView { } } - public void setCustomNavigationView(View view) { + public void setCustomView(View view) { final boolean showCustom = (mDisplayOptions & ActionBar.DISPLAY_SHOW_CUSTOM) != 0; if (showCustom) { ActionBarTransition.beginDelayedTransition(this); @@ -765,15 +753,16 @@ public class ActionBarView extends AbsActionBarView { } } - public void setDropdownAdapter(SpinnerAdapter adapter) { + public void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener l) { mSpinnerAdapter = adapter; + mNavItemSelectedListener = l; if (mSpinner != null) { mSpinner.setAdapter(adapter); } } - public SpinnerAdapter getDropdownAdapter() { - return mSpinnerAdapter; + public int getDropdownItemCount() { + return mSpinnerAdapter != null ? mSpinnerAdapter.getCount() : 0; } public void setDropdownSelectedPosition(int position) { @@ -784,7 +773,7 @@ public class ActionBarView extends AbsActionBarView { return mSpinner.getSelectedItemPosition(); } - public View getCustomNavigationView() { + public View getCustomView() { return mCustomNavView; } @@ -797,6 +786,11 @@ public class ActionBarView extends AbsActionBarView { } @Override + public ViewGroup getViewGroup() { + return this; + } + + @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { // Used by custom nav views if they don't supply layout params. Everything else // added to an ActionBarView should have them already. @@ -860,12 +854,8 @@ public class ActionBarView extends AbsActionBarView { mContextView = view; } - public void setCollapsable(boolean collapsable) { - mIsCollapsable = collapsable; - } - - public boolean isCollapsed() { - return mIsCollapsed; + public void setCollapsible(boolean collapsible) { + mIsCollapsible = collapsible; } /** @@ -893,7 +883,7 @@ public class ActionBarView extends AbsActionBarView { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int childCount = getChildCount(); - if (mIsCollapsable) { + if (mIsCollapsible) { int visibleChildren = 0; for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); @@ -915,11 +905,9 @@ public class ActionBarView extends AbsActionBarView { if (visibleChildren == 0) { // No size for an empty action bar when collapsable. setMeasuredDimension(0, 0); - mIsCollapsed = true; return; } } - mIsCollapsed = false; int widthMode = MeasureSpec.getMode(widthMeasureSpec); if (widthMode != MeasureSpec.EXACTLY) { @@ -1323,20 +1311,20 @@ public class ActionBarView extends AbsActionBarView { } } - public void setHomeAsUpIndicator(Drawable indicator) { + public void setNavigationIcon(Drawable indicator) { mHomeLayout.setUpIndicator(indicator); } - public void setHomeAsUpIndicator(int resId) { + public void setNavigationIcon(int resId) { mHomeLayout.setUpIndicator(resId); } - public void setHomeActionContentDescription(CharSequence description) { + public void setNavigationContentDescription(CharSequence description) { mHomeDescription = description; updateHomeAccessibility(mUpGoerFive.isEnabled()); } - public void setHomeActionContentDescription(int resId) { + public void setNavigationContentDescription(int resId) { mHomeDescriptionRes = resId; mHomeDescription = resId != 0 ? getResources().getText(resId) : null; updateHomeAccessibility(mUpGoerFive.isEnabled()); diff --git a/core/java/com/android/internal/widget/DecorContentParent.java b/core/java/com/android/internal/widget/DecorContentParent.java new file mode 100644 index 0000000..4fa370a --- /dev/null +++ b/core/java/com/android/internal/widget/DecorContentParent.java @@ -0,0 +1,53 @@ +/* + * 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.widget; + +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.Menu; +import android.view.Window; +import com.android.internal.view.menu.MenuPresenter; + +/** + * Implemented by the top-level decor layout for a window. DecorContentParent offers + * entry points for a number of title/window decor features. + */ +public interface DecorContentParent { + void setWindowCallback(Window.Callback cb); + void setWindowTitle(CharSequence title); + CharSequence getTitle(); + void initFeature(int windowFeature); + void setUiOptions(int uiOptions); + boolean hasIcon(); + boolean hasLogo(); + void setIcon(int resId); + void setIcon(Drawable d); + void setLogo(int resId); + boolean canShowOverflowMenu(); + boolean isOverflowMenuShowing(); + boolean isOverflowMenuShowPending(); + boolean showOverflowMenu(); + boolean hideOverflowMenu(); + void setMenuPrepared(); + void setMenu(Menu menu, MenuPresenter.Callback cb); + void saveToolbarHierarchyState(SparseArray<Parcelable> toolbarStates); + void restoreToolbarHierarchyState(SparseArray<Parcelable> toolbarStates); + void dismissPopups(); + +} diff --git a/core/java/com/android/internal/widget/DecorToolbar.java b/core/java/com/android/internal/widget/DecorToolbar.java new file mode 100644 index 0000000..ee6988e --- /dev/null +++ b/core/java/com/android/internal/widget/DecorToolbar.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 com.android.internal.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.AdapterView; +import android.widget.SpinnerAdapter; +import com.android.internal.view.menu.MenuPresenter; + +/** + * Common interface for a toolbar that sits as part of the window decor. + * Layouts that control window decor use this as a point of interaction with different + * bar implementations. + * + * @hide + */ +public interface DecorToolbar { + ViewGroup getViewGroup(); + Context getContext(); + boolean isSplit(); + boolean hasExpandedActionView(); + void collapseActionView(); + void setWindowCallback(Window.Callback cb); + void setWindowTitle(CharSequence title); + CharSequence getTitle(); + void setTitle(CharSequence title); + CharSequence getSubtitle(); + void setSubtitle(CharSequence subtitle); + void initProgress(); + void initIndeterminateProgress(); + boolean canSplit(); + void setSplitView(ViewGroup splitView); + void setSplitToolbar(boolean split); + void setSplitWhenNarrow(boolean splitWhenNarrow); + boolean hasIcon(); + boolean hasLogo(); + void setIcon(int resId); + void setIcon(Drawable d); + void setLogo(int resId); + void setLogo(Drawable d); + boolean canShowOverflowMenu(); + boolean isOverflowMenuShowing(); + boolean isOverflowMenuShowPending(); + boolean showOverflowMenu(); + boolean hideOverflowMenu(); + void setMenuPrepared(); + void setMenu(Menu menu, MenuPresenter.Callback cb); + void dismissPopupMenus(); + + int getDisplayOptions(); + void setDisplayOptions(int opts); + void setEmbeddedTabView(View tabView); + boolean hasEmbeddedTabs(); + boolean isTitleTruncated(); + void setCollapsible(boolean collapsible); + void setHomeButtonEnabled(boolean enable); + int getNavigationMode(); + void setNavigationMode(int mode); + void setDropdownParams(SpinnerAdapter adapter, AdapterView.OnItemSelectedListener listener); + void setDropdownSelectedPosition(int position); + int getDropdownSelectedPosition(); + int getDropdownItemCount(); + void setCustomView(View view); + View getCustomView(); + void animateToVisibility(int visibility); + void setNavigationIcon(Drawable icon); + void setNavigationIcon(int resId); + void setNavigationContentDescription(CharSequence description); + void setNavigationContentDescription(int resId); + void saveHierarchyState(SparseArray<Parcelable> toolbarStates); + void restoreHierarchyState(SparseArray<Parcelable> toolbarStates); +} diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index 9501f92..c70841b 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -16,6 +16,8 @@ package com.android.internal.widget; +import com.android.internal.widget.ILockSettingsObserver; + /** {@hide} */ interface ILockSettings { void setBoolean(in String key, in boolean value, in int userId); @@ -32,4 +34,6 @@ interface ILockSettings { boolean havePattern(int userId); boolean havePassword(int userId); void removeUser(int userId); + void registerObserver(in ILockSettingsObserver observer); + void unregisterObserver(in ILockSettingsObserver observer); } diff --git a/core/java/com/android/internal/widget/ILockSettingsObserver.aidl b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl new file mode 100644 index 0000000..6c354d8 --- /dev/null +++ b/core/java/com/android/internal/widget/ILockSettingsObserver.aidl @@ -0,0 +1,22 @@ +/* + * 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.widget; + +/** {@hide} */ +oneway interface ILockSettingsObserver { + void onLockSettingChanged(in String key, in int userId); +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 2882b54..d31c5cc 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -43,7 +43,6 @@ import android.view.View; import android.widget.Button; import com.android.internal.R; -import com.android.internal.telephony.ITelephony; import com.google.android.collect.Lists; import java.security.MessageDigest; @@ -199,8 +198,8 @@ public class LockPatternUtils { private ILockSettings getLockSettings() { if (mLockSettingsService == null) { - mLockSettingsService = ILockSettings.Stub.asInterface( - (IBinder) ServiceManager.getService("lock_settings")); + mLockSettingsService = LockPatternUtilsCache.getInstance( + ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"))); } return mLockSettingsService; } @@ -1360,19 +1359,11 @@ public class LockPatternUtils { /** * Resumes a call in progress. Typically launched from the EmergencyCall button * on various lockscreens. - * - * @return true if we were able to tell InCallScreen to show. */ - public boolean resumeCall() { - ITelephony phone = ITelephony.Stub.asInterface(ServiceManager.checkService("phone")); - try { - if (phone != null && phone.showCallScreen()) { - return true; - } - } catch (RemoteException e) { - // What can we do? - } - return false; + public void resumeCall() { + TelephonyManager telephonyManager = + (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE); + telephonyManager.showCallScreen(); } private void finishBiometricWeak() { diff --git a/core/java/com/android/internal/widget/LockPatternUtilsCache.java b/core/java/com/android/internal/widget/LockPatternUtilsCache.java new file mode 100644 index 0000000..624f67c --- /dev/null +++ b/core/java/com/android/internal/widget/LockPatternUtilsCache.java @@ -0,0 +1,243 @@ +/* + * 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.widget; + +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; + +/** + * A decorator for {@link ILockSettings} that caches the key-value responses in memory. + * + * Specifically, the return values of {@link #getString(String, String, int)}, + * {@link #getLong(String, long, int)} and {@link #getBoolean(String, boolean, int)} are cached. + */ +public class LockPatternUtilsCache implements ILockSettings { + + private static final String HAS_LOCK_PATTERN_CACHE_KEY + = "LockPatternUtils.Cache.HasLockPatternCacheKey"; + private static final String HAS_LOCK_PASSWORD_CACHE_KEY + = "LockPatternUtils.Cache.HasLockPasswordCacheKey"; + + private static LockPatternUtilsCache sInstance; + + private final ILockSettings mService; + + /** Only access when holding {@code mCache} lock. */ + private final ArrayMap<CacheKey, Object> mCache = new ArrayMap<>(); + + /** Only access when holding {@link #mCache} lock. */ + private final CacheKey mCacheKey = new CacheKey(); + + + public static synchronized LockPatternUtilsCache getInstance(ILockSettings service) { + if (sInstance == null) { + sInstance = new LockPatternUtilsCache(service); + } + return sInstance; + } + + // ILockSettings + + private LockPatternUtilsCache(ILockSettings service) { + mService = service; + try { + service.registerObserver(mObserver); + } catch (RemoteException e) { + // Not safe to do caching without the observer. System process has probably died + // anyway, so crashing here is fine. + throw new RuntimeException(e); + } + } + + public void setBoolean(String key, boolean value, int userId) throws RemoteException { + invalidateCache(key, userId); + mService.setBoolean(key, value, userId); + putCache(key, userId, value); + } + + public void setLong(String key, long value, int userId) throws RemoteException { + invalidateCache(key, userId); + mService.setLong(key, value, userId); + putCache(key, userId, value); + } + + public void setString(String key, String value, int userId) throws RemoteException { + invalidateCache(key, userId); + mService.setString(key, value, userId); + putCache(key, userId, value); + } + + public long getLong(String key, long defaultValue, int userId) throws RemoteException { + Object value = peekCache(key, userId); + if (value instanceof Long) { + return (long) value; + } + long result = mService.getLong(key, defaultValue, userId); + putCache(key, userId, result); + return result; + } + + public String getString(String key, String defaultValue, int userId) throws RemoteException { + Object value = peekCache(key, userId); + if (value instanceof String) { + return (String) value; + } + String result = mService.getString(key, defaultValue, userId); + putCache(key, userId, result); + return result; + } + + public boolean getBoolean(String key, boolean defaultValue, int userId) throws RemoteException { + Object value = peekCache(key, userId); + if (value instanceof Boolean) { + return (boolean) value; + } + boolean result = mService.getBoolean(key, defaultValue, userId); + putCache(key, userId, result); + return result; + } + + @Override + public void setLockPattern(String pattern, int userId) throws RemoteException { + invalidateCache(HAS_LOCK_PATTERN_CACHE_KEY, userId); + mService.setLockPattern(pattern, userId); + putCache(HAS_LOCK_PATTERN_CACHE_KEY, userId, pattern != null); + } + + @Override + public boolean checkPattern(String pattern, int userId) throws RemoteException { + return mService.checkPattern(pattern, userId); + } + + @Override + public void setLockPassword(String password, int userId) throws RemoteException { + invalidateCache(HAS_LOCK_PASSWORD_CACHE_KEY, userId); + mService.setLockPassword(password, userId); + putCache(HAS_LOCK_PASSWORD_CACHE_KEY, userId, password != null); + } + + @Override + public boolean checkPassword(String password, int userId) throws RemoteException { + return mService.checkPassword(password, userId); + } + + @Override + public boolean checkVoldPassword(int userId) throws RemoteException { + return mService.checkVoldPassword(userId); + } + + @Override + public boolean havePattern(int userId) throws RemoteException { + Object value = peekCache(HAS_LOCK_PATTERN_CACHE_KEY, userId); + if (value instanceof Boolean) { + return (boolean) value; + } + boolean result = mService.havePattern(userId); + putCache(HAS_LOCK_PATTERN_CACHE_KEY, userId, result); + return result; + } + + @Override + public boolean havePassword(int userId) throws RemoteException { + Object value = peekCache(HAS_LOCK_PASSWORD_CACHE_KEY, userId); + if (value instanceof Boolean) { + return (boolean) value; + } + boolean result = mService.havePassword(userId); + putCache(HAS_LOCK_PASSWORD_CACHE_KEY, userId, result); + return result; + } + + @Override + public void removeUser(int userId) throws RemoteException { + mService.removeUser(userId); + } + + @Override + public void registerObserver(ILockSettingsObserver observer) throws RemoteException { + mService.registerObserver(observer); + } + + @Override + public void unregisterObserver(ILockSettingsObserver observer) throws RemoteException { + mService.unregisterObserver(observer); + } + + @Override + public IBinder asBinder() { + return mService.asBinder(); + } + + // Caching + + private Object peekCache(String key, int userId) { + synchronized (mCache) { + // Safe to reuse mCacheKey, because it is not stored in the map. + return mCache.get(mCacheKey.set(key, userId)); + } + } + + private void putCache(String key, int userId, Object value) { + synchronized (mCache) { + // Create a new key, because this will be stored in the map. + mCache.put(new CacheKey().set(key, userId), value); + } + } + + private void invalidateCache(String key, int userId) { + synchronized (mCache) { + // Safe to reuse mCacheKey, because it is not stored in the map. + mCache.remove(mCacheKey.set(key, userId)); + } + } + + private final ILockSettingsObserver mObserver = new ILockSettingsObserver.Stub() { + @Override + public void onLockSettingChanged(String key, int userId) throws RemoteException { + invalidateCache(key, userId); + } + }; + + private static final class CacheKey { + String key; + int userId; + + public CacheKey set(String key, int userId) { + this.key = key; + this.userId = userId; + return this; + } + + public CacheKey copy() { + return new CacheKey().set(key, userId); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof CacheKey)) + return false; + CacheKey o = (CacheKey) obj; + return userId == o.userId && key.equals(o.key); + } + + @Override + public int hashCode() { + return key.hashCode() ^ userId; + } + } +} diff --git a/core/java/com/android/internal/widget/LockPatternView.java b/core/java/com/android/internal/widget/LockPatternView.java index 36ed344..d841d53 100644 --- a/core/java/com/android/internal/widget/LockPatternView.java +++ b/core/java/com/android/internal/widget/LockPatternView.java @@ -57,6 +57,7 @@ public class LockPatternView extends View { private static final int ASPECT_LOCK_HEIGHT = 2; // Fixed height; width will be minimum of (w,h) private static final boolean PROFILE_DRAWING = false; + private final CellState[][] mCellStates; private boolean mDrawingProfilingStarted = false; private Paint mPaint = new Paint(); @@ -187,6 +188,12 @@ public class LockPatternView extends View { } } + public static class CellState { + public float scale = 1.0f; + public float translateY = 0.0f; + public float alpha = 1.0f; + } + /** * How to display the current pattern. */ @@ -296,6 +303,18 @@ public class LockPatternView extends View { mBitmapHeight = Math.max(mBitmapHeight, bitmap.getHeight()); } + mPaint.setFilterBitmap(true); + + mCellStates = new CellState[3][3]; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + mCellStates[i][j] = new CellState(); + } + } + } + + public CellState[][] getCellStates() { + return mCellStates; } private Bitmap getBitmapFor(int resId) { @@ -873,18 +892,22 @@ public class LockPatternView extends View { //float centerY = mPaddingTop + i * mSquareHeight + (mSquareHeight / 2); for (int j = 0; j < 3; j++) { float leftX = paddingLeft + j * squareWidth; - drawCircle(canvas, (int) leftX, (int) topY, drawLookup[i][j]); + float scale = mCellStates[i][j].scale; + mPaint.setAlpha((int) (mCellStates[i][j].alpha * 255)); + float translationY = mCellStates[i][j].translateY; + drawCircle(canvas, (int) leftX, (int) topY + translationY, scale, drawLookup[i][j]); } } + // Reset the alpha to draw normally + mPaint.setAlpha(255); + // 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 we are in stealth mode) final boolean drawPath = !mInStealthMode; // 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) { for (int i = 0; i < count - 1; i++) { Cell cell = pattern.get(i); @@ -898,7 +921,8 @@ public class LockPatternView extends View { } float leftX = paddingLeft + cell.column * squareWidth; - float topY = paddingTop + cell.row * squareHeight; + float topY = paddingTop + cell.row * squareHeight + + mCellStates[cell.row][cell.column].translateY; drawArrow(canvas, leftX, topY, cell, next); } @@ -919,6 +943,9 @@ public class LockPatternView extends View { float centerX = getCenterXForColumn(cell.column); float centerY = getCenterYForRow(cell.row); + + // Respect translation in animation + centerY += mCellStates[cell.row][cell.column].translateY; if (i == 0) { currentPath.moveTo(centerX, centerY); } else { @@ -933,8 +960,6 @@ public class LockPatternView extends View { } canvas.drawPath(currentPath, mPathPaint); } - - mPaint.setFilterBitmap(oldFlag); // restore default flag } private void drawArrow(Canvas canvas, float leftX, float topY, Cell start, Cell end) { @@ -979,7 +1004,8 @@ public class LockPatternView extends View { * @param topY * @param partOfPattern Whether this circle is part of the pattern. */ - private void drawCircle(Canvas canvas, int leftX, int topY, boolean partOfPattern) { + private void drawCircle(Canvas canvas, float leftX, float topY, float scale, + boolean partOfPattern) { Bitmap outerCircle; Bitmap innerCircle; @@ -1019,7 +1045,7 @@ public class LockPatternView extends View { mCircleMatrix.setTranslate(leftX + offsetX, topY + offsetY); mCircleMatrix.preTranslate(mBitmapWidth/2, mBitmapHeight/2); - mCircleMatrix.preScale(sx, sy); + mCircleMatrix.preScale(sx * scale, sy * scale); mCircleMatrix.preTranslate(-mBitmapWidth/2, -mBitmapHeight/2); canvas.drawBitmap(outerCircle, mCircleMatrix, mPaint); diff --git a/core/java/com/android/internal/widget/SwipeDismissLayout.java b/core/java/com/android/internal/widget/SwipeDismissLayout.java index bcfa036..002573e 100644 --- a/core/java/com/android/internal/widget/SwipeDismissLayout.java +++ b/core/java/com/android/internal/widget/SwipeDismissLayout.java @@ -35,7 +35,7 @@ import android.widget.FrameLayout; public class SwipeDismissLayout extends FrameLayout { private static final String TAG = "SwipeDismissLayout"; - private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .4f; + private static final float DISMISS_MIN_DRAG_WIDTH_RATIO = .33f; public interface OnDismissedListener { void onDismissed(SwipeDismissLayout layout); diff --git a/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java new file mode 100644 index 0000000..3e15c32 --- /dev/null +++ b/core/java/com/android/internal/widget/ToolbarWidgetWrapper.java @@ -0,0 +1,541 @@ +/* + * 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.widget; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.app.ActionBar; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ActionMenuPresenter; +import android.widget.AdapterView; +import android.widget.Spinner; +import android.widget.SpinnerAdapter; +import android.widget.Toolbar; +import com.android.internal.R; +import com.android.internal.view.menu.ActionMenuItem; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuPresenter; + +/** + * Internal class used to interact with the Toolbar widget without + * exposing interface methods to the public API. + * + * <p>ToolbarWidgetWrapper manages the differences between Toolbar and ActionBarView + * so that either variant acting as a + * {@link com.android.internal.app.WindowDecorActionBar WindowDecorActionBar} can behave + * in the same way.</p> + * + * @hide + */ +public class ToolbarWidgetWrapper implements DecorToolbar { + private static final String TAG = "ToolbarWidgetWrapper"; + + private static final int AFFECTS_LOGO_MASK = + ActionBar.DISPLAY_SHOW_HOME | ActionBar.DISPLAY_USE_LOGO; + + private Toolbar mToolbar; + + private int mDisplayOpts; + private View mTabView; + private Spinner mSpinner; + private View mCustomView; + + private Drawable mIcon; + private Drawable mLogo; + private Drawable mNavIcon; + + private boolean mTitleSet; + private CharSequence mTitle; + private CharSequence mSubtitle; + + private Window.Callback mWindowCallback; + private boolean mMenuPrepared; + private ActionMenuPresenter mActionMenuPresenter; + + public ToolbarWidgetWrapper(Toolbar toolbar) { + mToolbar = toolbar; + + mTitle = toolbar.getTitle(); + mSubtitle = toolbar.getSubtitle(); + mTitleSet = !TextUtils.isEmpty(mTitle); + + final TypedArray a = toolbar.getContext().obtainStyledAttributes(null, + R.styleable.ActionBar, R.attr.actionBarStyle, 0); + + final CharSequence title = a.getText(R.styleable.ActionBar_title); + if (!TextUtils.isEmpty(title)) { + setTitle(title); + } + + final CharSequence subtitle = a.getText(R.styleable.ActionBar_subtitle); + if (!TextUtils.isEmpty(subtitle)) { + setSubtitle(subtitle); + } + + final Drawable logo = a.getDrawable(R.styleable.ActionBar_logo); + if (logo != null) { + setLogo(logo); + } + + final Drawable icon = a.getDrawable(R.styleable.ActionBar_icon); + if (icon != null) { + setIcon(icon); + } + + final Drawable navIcon = a.getDrawable(R.styleable.ActionBar_homeAsUpIndicator); + if (navIcon != null) { + setNavigationIcon(navIcon); + } + + setDisplayOptions(a.getInt(R.styleable.ActionBar_displayOptions, 0)); + + final int customNavId = a.getResourceId(R.styleable.ActionBar_customNavigationLayout, 0); + if (customNavId != 0) { + setCustomView(LayoutInflater.from(mToolbar.getContext()).inflate(customNavId, + mToolbar, false)); + setDisplayOptions(mDisplayOpts | ActionBar.DISPLAY_SHOW_CUSTOM); + } + + final int height = a.getLayoutDimension(R.styleable.ActionBar_height, 0); + if (height > 0) { + final ViewGroup.LayoutParams lp = mToolbar.getLayoutParams(); + lp.height = height; + mToolbar.setLayoutParams(lp); + } + + final int contentInsetStart = a.getDimensionPixelOffset( + R.styleable.ActionBar_contentInsetStart, 0); + final int contentInsetEnd = a.getDimensionPixelOffset( + R.styleable.ActionBar_contentInsetEnd, 0); + if (contentInsetStart > 0 || contentInsetEnd > 0) { + mToolbar.setContentInsetsRelative(contentInsetStart, contentInsetEnd); + } + + final int titleTextStyle = a.getResourceId(R.styleable.ActionBar_titleTextStyle, 0); + if (titleTextStyle != 0) { + mToolbar.setTitleTextAppearance(mToolbar.getContext(), titleTextStyle); + } + + final int subtitleTextStyle = a.getResourceId(R.styleable.ActionBar_subtitleTextStyle, 0); + if (subtitleTextStyle != 0) { + mToolbar.setSubtitleTextAppearance(mToolbar.getContext(), subtitleTextStyle); + } + + a.recycle(); + + mToolbar.setNavigationOnClickListener(new View.OnClickListener() { + final ActionMenuItem mNavItem = new ActionMenuItem(mToolbar.getContext(), + 0, android.R.id.home, 0, 0, mTitle); + @Override + public void onClick(View v) { + if (mWindowCallback != null && mMenuPrepared) { + mWindowCallback.onMenuItemSelected(Window.FEATURE_OPTIONS_PANEL, mNavItem); + } + } + }); + } + + @Override + public ViewGroup getViewGroup() { + return mToolbar; + } + + @Override + public Context getContext() { + return mToolbar.getContext(); + } + + @Override + public boolean isSplit() { + return false; + } + + @Override + public boolean hasExpandedActionView() { + return mToolbar.hasExpandedActionView(); + } + + @Override + public void collapseActionView() { + mToolbar.collapseActionView(); + } + + @Override + public void setWindowCallback(Window.Callback cb) { + mWindowCallback = cb; + } + + @Override + public void setWindowTitle(CharSequence title) { + // "Real" title always trumps window title. + if (!mTitleSet) { + setTitleInt(title); + } + } + + @Override + public CharSequence getTitle() { + return mToolbar.getTitle(); + } + + @Override + public void setTitle(CharSequence title) { + mTitleSet = true; + setTitleInt(title); + } + + private void setTitleInt(CharSequence title) { + mTitle = title; + if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + mToolbar.setTitle(title); + } + } + + @Override + public CharSequence getSubtitle() { + return mToolbar.getSubtitle(); + } + + @Override + public void setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + mToolbar.setSubtitle(subtitle); + } + } + + @Override + public void initProgress() { + Log.i(TAG, "Progress display unsupported"); + } + + @Override + public void initIndeterminateProgress() { + Log.i(TAG, "Progress display unsupported"); + } + + @Override + public boolean canSplit() { + return false; + } + + @Override + public void setSplitView(ViewGroup splitView) { + } + + @Override + public void setSplitToolbar(boolean split) { + if (split) { + throw new UnsupportedOperationException("Cannot split an android.widget.Toolbar"); + } + } + + @Override + public void setSplitWhenNarrow(boolean splitWhenNarrow) { + // Ignore. + } + + @Override + public boolean hasIcon() { + return mIcon != null; + } + + @Override + public boolean hasLogo() { + return mLogo != null; + } + + @Override + public void setIcon(int resId) { + setIcon(resId != 0 ? getContext().getDrawable(resId) : null); + } + + @Override + public void setIcon(Drawable d) { + mIcon = d; + updateToolbarLogo(); + } + + @Override + public void setLogo(int resId) { + setLogo(resId != 0 ? getContext().getDrawable(resId) : null); + } + + @Override + public void setLogo(Drawable d) { + mLogo = d; + updateToolbarLogo(); + } + + private void updateToolbarLogo() { + Drawable logo = null; + if ((mDisplayOpts & ActionBar.DISPLAY_SHOW_HOME) != 0) { + if ((mDisplayOpts & ActionBar.DISPLAY_USE_LOGO) != 0) { + logo = mLogo != null ? mLogo : mIcon; + } else { + logo = mIcon; + } + } + mToolbar.setLogo(logo); + } + + @Override + public boolean canShowOverflowMenu() { + return mToolbar.canShowOverflowMenu(); + } + + @Override + public boolean isOverflowMenuShowing() { + return mToolbar.isOverflowMenuShowing(); + } + + @Override + public boolean isOverflowMenuShowPending() { + return mToolbar.isOverflowMenuShowPending(); + } + + @Override + public boolean showOverflowMenu() { + return mToolbar.showOverflowMenu(); + } + + @Override + public boolean hideOverflowMenu() { + return mToolbar.hideOverflowMenu(); + } + + @Override + public void setMenuPrepared() { + mMenuPrepared = true; + } + + @Override + public void setMenu(Menu menu, MenuPresenter.Callback cb) { + if (mActionMenuPresenter == null) { + mActionMenuPresenter = new ActionMenuPresenter(mToolbar.getContext()); + mActionMenuPresenter.setId(com.android.internal.R.id.action_menu_presenter); + } + mActionMenuPresenter.setCallback(cb); + mToolbar.setMenu((MenuBuilder) menu, mActionMenuPresenter); + } + + @Override + public void dismissPopupMenus() { + mToolbar.dismissPopupMenus(); + } + + @Override + public int getDisplayOptions() { + return mDisplayOpts; + } + + @Override + public void setDisplayOptions(int newOpts) { + final int oldOpts = mDisplayOpts; + final int changed = oldOpts ^ newOpts; + mDisplayOpts = newOpts; + if (changed != 0) { + if ((changed & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + if ((newOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + mToolbar.setNavigationIcon(mNavIcon); + } else { + mToolbar.setNavigationIcon(null); + } + } + + if ((changed & AFFECTS_LOGO_MASK) != 0) { + updateToolbarLogo(); + } + + if ((changed & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + if ((newOpts & ActionBar.DISPLAY_SHOW_TITLE) != 0) { + mToolbar.setTitle(mTitle); + mToolbar.setSubtitle(mSubtitle); + } else { + mToolbar.setTitle(null); + mToolbar.setSubtitle(null); + } + } + + if ((changed & ActionBar.DISPLAY_SHOW_CUSTOM) != 0 && mCustomView != null) { + if ((newOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { + mToolbar.addView(mCustomView); + } else { + mToolbar.removeView(mCustomView); + } + } + } + } + + @Override + public void setEmbeddedTabView(View tabView) { + mTabView = tabView; + } + + @Override + public boolean hasEmbeddedTabs() { + return mTabView != null; + } + + @Override + public boolean isTitleTruncated() { + return mToolbar.isTitleTruncated(); + } + + @Override + public void setCollapsible(boolean collapsible) { + // Ignore + } + + @Override + public void setHomeButtonEnabled(boolean enable) { + // Ignore + } + + @Override + public int getNavigationMode() { + return 0; + } + + @Override + public void setNavigationMode(int mode) { + if (mode != ActionBar.NAVIGATION_MODE_STANDARD) { + throw new IllegalArgumentException( + "Navigation modes not supported in this configuration"); + } + } + + @Override + public void setDropdownParams(SpinnerAdapter adapter, + AdapterView.OnItemSelectedListener listener) { + if (mSpinner == null) { + mSpinner = new Spinner(getContext()); + } + mSpinner.setAdapter(adapter); + mSpinner.setOnItemSelectedListener(listener); + } + + @Override + public void setDropdownSelectedPosition(int position) { + if (mSpinner == null) { + throw new IllegalStateException( + "Can't set dropdown selected position without an adapter"); + } + mSpinner.setSelection(position); + } + + @Override + public int getDropdownSelectedPosition() { + return mSpinner != null ? mSpinner.getSelectedItemPosition() : 0; + } + + @Override + public int getDropdownItemCount() { + return mSpinner != null ? mSpinner.getCount() : 0; + } + + @Override + public void setCustomView(View view) { + if (mCustomView != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { + mToolbar.removeView(mCustomView); + } + mCustomView = view; + if (view != null && (mDisplayOpts & ActionBar.DISPLAY_SHOW_CUSTOM) != 0) { + mToolbar.addView(mCustomView); + } + } + + @Override + public View getCustomView() { + return mCustomView; + } + + @Override + public void animateToVisibility(int visibility) { + if (visibility == View.GONE) { + mToolbar.animate().translationY(mToolbar.getHeight()).alpha(0) + .setListener(new AnimatorListenerAdapter() { + private boolean mCanceled = false; + @Override + public void onAnimationEnd(Animator animation) { + if (!mCanceled) { + mToolbar.setVisibility(View.GONE); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mCanceled = true; + } + }); + } else if (visibility == View.VISIBLE) { + mToolbar.animate().translationY(0).alpha(1) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + mToolbar.setVisibility(View.VISIBLE); + } + }); + } + } + + @Override + public void setNavigationIcon(Drawable icon) { + mNavIcon = icon; + if ((mDisplayOpts & ActionBar.DISPLAY_HOME_AS_UP) != 0) { + mToolbar.setNavigationIcon(icon); + } + } + + @Override + public void setNavigationIcon(int resId) { + setNavigationIcon(mToolbar.getContext().getDrawable(resId)); + } + + @Override + public void setNavigationContentDescription(CharSequence description) { + mToolbar.setNavigationContentDescription(description); + } + + @Override + public void setNavigationContentDescription(int resId) { + mToolbar.setNavigationContentDescription(resId); + } + + @Override + public void saveHierarchyState(SparseArray<Parcelable> toolbarStates) { + mToolbar.saveHierarchyState(toolbarStates); + } + + @Override + public void restoreHierarchyState(SparseArray<Parcelable> toolbarStates) { + mToolbar.restoreHierarchyState(toolbarStates); + } + +} diff --git a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java index 772dc5f..841a02a 100644 --- a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java +++ b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java @@ -238,6 +238,10 @@ public class GlowPadView extends View { Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null; mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f); + mPointCloud = new PointCloud(pointDrawable); + mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); + mPointCloud.glowManager.setRadius(mGlowRadius); + TypedValue outValue = new TypedValue(); // Read array of target drawables @@ -273,10 +277,6 @@ public class GlowPadView extends View { setVibrateEnabled(mVibrationDuration > 0); assignDefaultsIfNeeded(); - - mPointCloud = new PointCloud(pointDrawable); - mPointCloud.makePointCloud(mInnerRadius, mOuterRadius); - mPointCloud.glowManager.setRadius(mGlowRadius); } private int getResourceId(TypedArray a, int id) { |
