diff options
Diffstat (limited to 'core/java/android')
337 files changed, 28446 insertions, 14104 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 4a30b05..23b5f29 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -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,6 +921,9 @@ public class Activity extends ContextThemeWrapper } mFragments.dispatchCreate(); getApplication().dispatchActivityCreated(this, savedInstanceState); + if (mVoiceInteractor != null) { + mVoiceInteractor.attachActivity(this); + } mCalled = true; } @@ -1100,9 +1106,6 @@ public class Activity extends ContextThemeWrapper mTitleReady = true; onTitleChanged(getTitle(), getTitleColor()); } - if (mEnterTransitionCoordinator != null) { - mEnterTransitionCoordinator.readyToEnter(); - } mCalled = true; } @@ -1149,12 +1152,6 @@ public class Activity extends ContextThemeWrapper } getApplication().dispatchActivityStarted(this); - - final ActivityOptions activityOptions = getActivityOptions(); - if (activityOptions != null && - activityOptions.getAnimationType() == ActivityOptions.ANIM_SCENE_TRANSITION) { - mEnterTransitionCoordinator = activityOptions.createEnterActivityTransition(this); - } } /** @@ -1204,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; } @@ -1279,6 +1276,7 @@ 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); } @@ -1549,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; @@ -1839,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; } @@ -1848,6 +1844,7 @@ public class Activity extends ContextThemeWrapper nci.children = children; nci.fragments = fragments; nci.loaders = mAllLoaderManagers; + nci.voiceInteractor = mVoiceInteractor; return nci; } @@ -2078,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(); } /** @@ -2452,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(); } } @@ -2663,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; @@ -2910,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); + } } /** @@ -3120,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); + } } /** @@ -3622,15 +3636,15 @@ public class Activity extends ContextThemeWrapper theme.applyStyle(resid, false); } - // Get the primary color and update the RecentsActivityValues for this activity + // 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.RecentsActivityValues v = new ActivityManager.RecentsActivityValues(); - v.colorPrimary = colorPrimary; - setRecentsActivityValues(v); + ActivityManager.TaskDescription v = new ActivityManager.TaskDescription(null, null, + colorPrimary); + setTaskDescription(v); } } } @@ -3650,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); } @@ -3691,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 = @@ -4205,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); } /** @@ -4230,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, @@ -4559,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(); } } @@ -4643,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 @@ -4919,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) { } } @@ -5246,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 @@ -5544,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 listen to events in the entering transition. + * @param listener Used to manipulate shared element transitions on the launched Activity. */ - public void setActivityTransitionListener(ActivityOptions.ActivityTransitionListener listener) { - if (mEnterTransitionCoordinator != null) { - mEnterTransitionCoordinator.setActivityTransitionListener(listener); + 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 manipulate shared element transitions on the launching Activity. + */ + public void setExitSharedElementListener(SharedElementListener listener) { + if (listener == null) { + listener = SharedElementListener.NULL_LISTENER; + } + mExitTransitionListener = listener; } // ------------------ Internal API ------------------ @@ -5598,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), @@ -5621,19 +5684,23 @@ public class Activity extends ContextThemeWrapper 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(); @@ -5656,6 +5723,7 @@ public class Activity extends ContextThemeWrapper lm.doReportStart(); } } + mActivityTransitionState.enterReady(this); } final void performRestart() { @@ -5803,6 +5871,9 @@ public class Activity extends ContextThemeWrapper if (mLoaderManager != null) { mLoaderManager.doDestroy(); } + if (mVoiceInteractor != null) { + mVoiceInteractor.detachActivity(); + } } /** @@ -5866,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 */ 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 2f924d3..56462ae 100644 --- a/core/java/android/app/ActivityManagerNative.java +++ b/core/java/android/app/ActivityManagerNative.java @@ -506,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(); @@ -929,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; @@ -2144,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; } @@ -2683,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(); @@ -2698,7 +2734,7 @@ class ActivityManagerProxy implements IActivityManager while (N > 0) { ActivityManager.RunningTaskInfo info = ActivityManager.RunningTaskInfo.CREATOR - .createFromParcel(reply); + .createFromParcel(reply); list.add(info); N--; } @@ -3305,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); @@ -3316,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; @@ -4933,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 692efd7..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,40 +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) { - if (!window.hasFeature(Window.FEATURE_CONTENT_TRANSITIONS)) { + 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; } @@ -413,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; } } @@ -470,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); } } @@ -493,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; } /** @@ -517,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; @@ -562,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; } } @@ -608,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; @@ -630,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 71e4e82..d9adba3 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -3010,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; } diff --git a/core/java/android/app/ActivityTransitionCoordinator.java b/core/java/android/app/ActivityTransitionCoordinator.java index ca64788..b658597 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().findNamedViews(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.getViewName()); - - 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.setViewName(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]) { @@ -918,8 +563,9 @@ abstract class ActivityTransitionCoordinator extends ResultReceiver { public void setEpicenter(Rect epicenter) { mEpicenter = epicenter; } @Override - public Rect getEpicenter(Transition transition) { + public Rect onGetEpicenter(Transition transition) { 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/ActivityView.java b/core/java/android/app/ActivityView.java index 097c64e..c29d75e 100644 --- a/core/java/android/app/ActivityView.java +++ b/core/java/android/app/ActivityView.java @@ -53,6 +53,7 @@ public class ActivityView extends ViewGroup { private int mHeight; private Surface mSurface; private int mLastVisibility; + private ActivityViewCallback mActivityViewCallback; // Only one IIntentSender or Intent may be queued at a time. Most recent one wins. IIntentSender mQueuedPendingIntent; @@ -254,6 +255,25 @@ public class ActivityView extends ViewGroup { } } + /** + * Set the callback to use to report certain state changes. + * @param callback The callback to report events to. + * + * @see ActivityViewCallback + */ + public void setCallback(ActivityViewCallback callback) { + mActivityViewCallback = callback; + } + + public static abstract class ActivityViewCallback { + /** + * Called when all activities in the ActivityView have completed and been removed. Register + * using {@link ActivityView#setCallback(ActivityViewCallback)}. Each ActivityView may + * have at most one callback registered. + */ + public abstract void onAllActivitiesComplete(ActivityView view); + } + private class ActivityViewSurfaceTextureListener implements SurfaceTextureListener { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, @@ -313,6 +333,22 @@ public class ActivityView extends ViewGroup { if (DEBUG) Log.v(TAG, "setVisible(): container=" + container + " visible=" + visible + " ActivityView=" + mActivityViewWeakReference.get()); } + + @Override + public void onAllActivitiesComplete(IBinder container) { + final ActivityView activityView = mActivityViewWeakReference.get(); + if (activityView != null) { + final ActivityViewCallback callback = activityView.mActivityViewCallback; + if (callback != null) { + activityView.post(new Runnable() { + @Override + public void run() { + callback.onAllActivitiesComplete(activityView); + } + }); + } + } + } } private static class ActivityContainerWrapper { 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/ContextImpl.java b/core/java/android/app/ContextImpl.java index b4d8942..e03224c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -35,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; @@ -58,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; @@ -70,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; @@ -81,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; @@ -113,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; @@ -131,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; @@ -245,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 = {}; @@ -382,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) { @@ -578,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() { @@ -599,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) { @@ -642,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); @@ -657,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() { @@ -684,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) { @@ -1015,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); } @@ -1345,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) { @@ -1368,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) { } } @@ -1708,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. } diff --git a/core/java/android/app/EnterTransitionCoordinator.java b/core/java/android/app/EnterTransitionCoordinator.java index d2d8ed1..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; } - } - }, null); - 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(); - } - }, null); - } 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/IActivityContainerCallback.aidl b/core/java/android/app/IActivityContainerCallback.aidl index 7f6d2c3..99d0a6f 100644 --- a/core/java/android/app/IActivityContainerCallback.aidl +++ b/core/java/android/app/IActivityContainerCallback.aidl @@ -21,4 +21,5 @@ import android.os.IBinder; /** @hide */ interface IActivityContainerCallback { oneway void setVisible(IBinder container, boolean visible); + oneway void onAllActivitiesComplete(IBinder container); } diff --git a/core/java/android/app/IActivityManager.java b/core/java/android/app/IActivityManager.java index d259b30..bf2d7e5 100644 --- a/core/java/android/app/IActivityManager.java +++ b/core/java/android/app/IActivityManager.java @@ -112,6 +112,7 @@ public interface IActivityManager extends IInterface { 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; @@ -175,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; @@ -439,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; /* @@ -738,7 +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/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/Notification.java b/core/java/android/app/Notification.java index 76a6a8e..5ac2a33 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -16,16 +16,17 @@ 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; import android.content.res.Resources; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.media.AudioManager; +import android.media.session.MediaSessionToken; import android.net.Uri; import android.os.BadParcelableException; import android.os.Build; @@ -34,17 +35,25 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; import android.os.UserHandle; +import android.os.UserManager; 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 +143,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 +379,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 +556,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 +694,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"; /** @@ -678,6 +728,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 +768,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 +957,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 +969,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 +1192,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 +1273,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 +1428,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 +1569,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 +1663,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 +1675,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 +1697,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 +1714,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 +1730,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 +2103,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 +2177,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 +2222,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 +2258,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 +2280,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; @@ -1885,30 +2309,46 @@ public class Notification implements Parcelable return this; } + private Bitmap getProfileBadge() { + UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); + Drawable badge = userManager.getBadgeForUser(android.os.Process.myUserHandle()); + if (badge == null) { + return null; + } + final int width = badge.getIntrinsicWidth(); + final int height = badge.getIntrinsicHeight(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + badge.setBounds(0, 0, width, height); + badge.draw(canvas); + return bitmap; + } + private RemoteViews applyStandardTemplate(int resId, boolean fitIn1U) { + Bitmap profileIcon = getProfileBadge(); 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 (profileIcon != null) { + contentView.setImageViewBitmap(R.id.profile_icon, profileIcon); + contentView.setViewVisibility(R.id.profile_icon, View.VISIBLE); + } else { + contentView.setViewVisibility(R.id.profile_icon, View.GONE); + } 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 +2435,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 +2541,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 +2562,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 +2636,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 +3149,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/ITvInputClient.aidl b/core/java/android/app/PackageUninstallObserver.java index ac83356..0a960a7 100644 --- a/core/java/android/tv/ITvInputClient.aidl +++ b/core/java/android/app/PackageUninstallObserver.java @@ -14,19 +14,24 @@ * limitations under the License. */ -package android.tv; +package android.app; -import android.content.ComponentName; -import android.tv.ITvInputSession; -import android.view.InputChannel; +import android.content.pm.IPackageDeleteObserver; -/** - * 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 String inputId, IBinder token, in InputChannel channel, int seq); - void onAvailabilityChanged(in String inputId, boolean isAvailable); - void onSessionReleased(int seq); +/** {@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/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..2cc15e2 100644 --- a/core/java/android/app/admin/DeviceAdminReceiver.java +++ b/core/java/android/app/admin/DeviceAdminReceiver.java @@ -166,12 +166,14 @@ 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> The intent may contain the extra + * {@link DevicePolicyManager#EXTRA_PROVISIONING_EMAIL_ADDRESS}. * * <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 8884446..57e0f63 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -25,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; @@ -32,6 +33,8 @@ 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; @@ -80,6 +83,20 @@ public class DevicePolicyManager { } /** + * Activity action: Used to indicate that the receiving activity is being started as part of the + * managed profile provisioning flow. This intent is typically sent to a mobile device + * management application (mdm) after the first part of the provisioning process is complete in + * the expectation that this app will (after optionally showing it's own UI) ultimately call + * {@link #ACTION_PROVISION_MANAGED_PROFILE} to complete the creation of the managed profile. + * + * <p> The intent may contain the extras {@link #EXTRA_PROVISIONING_TOKEN} and + * {@link #EXTRA_PROVISIONING_EMAIL_ADDRESS}. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + public static final String ACTION_SEND_PROVISIONING_VALUES + = "android.app.action.ACTION_SEND_PROVISIONING_VALUES"; + + /** * Activity action: Starts the provisioning flow which sets up a managed profile. * This intent will typically be sent by a mobile device management application(mdm). * Managed profile provisioning creates a profile, moves the mdm to the profile, @@ -101,6 +118,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 +137,18 @@ public class DevicePolicyManager { = "deviceAdminPackageName"; /** + * An int extra used to identify that during the current setup process the user has already + * consented to setting up a managed profile. This is typically received by + * a mobile device management application when it is started with + * {@link #ACTION_SEND_PROVISIONING_VALUES} and passed on in an intent + * {@link #ACTION_PROVISION_MANAGED_PROFILE} which starts the setup of the managed profile. The + * token indicates that steps asking for user consent can be skipped as the user has previously + * consented. + */ + 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} @@ -117,6 +157,17 @@ public class DevicePolicyManager { = "defaultManagedProfileName"; /** + * A String extra holding the email address of the profile that is created during managed + * profile provisioning. This is typically received by a mobile management application when it + * is started with {@link #ACTION_SEND_PROVISIONING_VALUES} and passed on in an intent + * {@link #ACTION_PROVISION_MANAGED_PROFILE} which starts the setup of the managed profile. It + * is eventually passed on in an intent + * {@link DeviceAdminReceiver#ACTION_PROFILE_PROVISIONING_COMPLETE}. + */ + public static final String EXTRA_PROVISIONING_EMAIL_ADDRESS + = "android.app.extra.ManagedProfileEmailAddress"; + + /** * Activity action: ask the user to add a new device administrator to the system. * The desired policy is the ComponentName of the policy in the * {@link #EXTRA_DEVICE_ADMIN} extra field. This will invoke a UI to @@ -174,15 +225,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 @@ -1949,17 +2001,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); } @@ -1967,14 +2018,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); } @@ -1982,6 +2033,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. * @@ -2050,45 +2138,68 @@ 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 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 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. * * @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 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 enableSystemApp(ComponentName admin, Intent intent) { + public int setApplicationsBlocked(ComponentName admin, Intent intent, boolean blocked) { if (mService != null) { try { - return mService.enableSystemAppWithIntent(admin, intent); + return mService.setApplicationsBlocked(admin, intent, blocked); } 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; } /** + * Called by device or profile owner to determine if a package is blocked. + * + * @param admin Which {@link DeviceAdminReceiver} this request is associated with. + * @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 boolean isApplicationBlocked(ComponentName admin, String packageName) { + if (mService != null) { + try { + return mService.isApplicationBlocked(admin, packageName); + } catch (RemoteException e) { + 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. * * <p>The calling device admin must be a profile owner. If it is not, a @@ -2181,4 +2292,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 407880f..87d57fb 100644 --- a/core/java/android/content/Task.java +++ b/core/java/android/app/task/Task.java @@ -14,26 +14,42 @@ * 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; +import android.os.PersistableBundle; /** - * 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; } /** + * Amount of backoff a task has initially by default, in milliseconds. + * @hide. + */ + public static final long DEFAULT_INITIAL_BACKOFF_MILLIS = 5000L; + + /** + * Default type of backoff. + * @hide + */ + public static final int DEFAULT_BACKOFF_POLICY = BackoffPolicy.EXPONENTIAL; + + /** * Linear: retry_time(failure_time, t) = failure_time + initial_retry_delay * t, t >= 1 * Expon: retry_time(failure_time, t) = failure_time + initial_retry_delay ^ t, t >= 1 */ @@ -44,10 +60,12 @@ public class Task implements Parcelable { private final int taskId; // TODO: Change this to use PersistableBundle when that lands in master. - private final Bundle extras; + private final PersistableBundle 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; @@ -59,14 +77,14 @@ public class Task implements Parcelable { /** * Unique task id associated with this class. This is assigned to your task by the scheduler. */ - public int getTaskId() { + public int getId() { return taskId; } /** * Bundle of extras which are returned to your application at execution time. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -92,7 +110,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; @@ -139,16 +157,34 @@ 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; } + /** + * 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(); + extras = in.readPersistableBundle(); service = ComponentName.readFromParcel(in); requireCharging = in.readInt() == 1; requireDeviceIdle = in.readInt() == 1; @@ -159,11 +195,13 @@ 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); + extras = new PersistableBundle(b.mExtras); service = b.mTaskService; requireCharging = b.mRequiresCharging; requireDeviceIdle = b.mRequiresDeviceIdle; @@ -174,6 +212,8 @@ public class Task implements Parcelable { intervalMillis = b.mIntervalMillis; initialBackoffMillis = b.mInitialBackoffMillis; backoffPolicy = b.mBackoffPolicy; + hasEarlyConstraint = b.mHasEarlyConstraint; + hasLateConstraint = b.mHasLateConstraint; } @Override @@ -184,7 +224,7 @@ public class Task implements Parcelable { @Override public void writeToParcel(Parcel out, int flags) { out.writeInt(taskId); - out.writeBundle(extras); + out.writePersistableBundle(extras); ComponentName.writeToParcel(service, out); out.writeInt(requireCharging ? 1 : 0); out.writeInt(requireDeviceIdle ? 1 : 0); @@ -195,6 +235,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>() { @@ -209,12 +251,10 @@ public class Task implements Parcelable { } }; - /** - * Builder class for constructing {@link Task} objects. - */ - public final class Builder { + /** Builder class for constructing {@link Task} objects. */ + public static final class Builder { private int mTaskId; - private Bundle mExtras; + private PersistableBundle mExtras = PersistableBundle.EMPTY; private ComponentName mTaskService; // Requirements. private boolean mRequiresCharging; @@ -225,10 +265,12 @@ 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; - private int mBackoffPolicy = BackoffPolicy.EXPONENTIAL; + private long mInitialBackoffMillis = DEFAULT_INITIAL_BACKOFF_MILLIS; + private int mBackoffPolicy = DEFAULT_BACKOFF_POLICY; /** Easy way to track whether the client has tried to set a back-off policy. */ private boolean mBackoffPolicySet = false; @@ -248,14 +290,14 @@ public class Task implements Parcelable { * Set optional extras. This is persisted, so we only allow primitive types. * @param extras Bundle containing extras you want the scheduler to hold on to for you. */ - public Builder setExtras(Bundle extras) { + public Builder setExtras(PersistableBundle extras) { mExtras = extras; return this; } /** * Set some description of the kind of network capabilities you would like to have. This - * will be a parameter defined in {@link android.content.Task.NetworkType}. + * 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 @@ -307,6 +349,7 @@ public class Task implements Parcelable { public Builder setPeriodic(long intervalMillis) { mIsPeriodic = true; mIntervalMillis = intervalMillis; + mHasEarlyConstraint = mHasLateConstraint = true; return this; } @@ -314,12 +357,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 +372,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,25 +405,7 @@ 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); - } + mExtras = new PersistableBundle(mExtras); // Make our own copy. // Check that a deadline was not set on a periodic task. if (mIsPeriodic && (mMaxExecutionDelayMillis != 0L)) { throw new IllegalArgumentException("Can't call setOverrideDeadline() on a " + 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 0351082..f4908c6 100644 --- a/core/java/android/app/task/TaskParams.java +++ b/core/java/android/app/task/TaskParams.java @@ -16,10 +16,10 @@ package android.app.task; -import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; +import android.os.PersistableBundle; /** * Contains the parameters used to configure/identify your task. You do not create this object @@ -28,11 +28,11 @@ import android.os.Parcelable; public class TaskParams implements Parcelable { private final int taskId; - private final Bundle extras; + private final PersistableBundle extras; private final IBinder callback; /** @hide */ - public TaskParams(int taskId, Bundle extras, IBinder callback) { + public TaskParams(int taskId, PersistableBundle extras, IBinder callback) { this.taskId = taskId; this.extras = extras; this.callback = callback; @@ -47,10 +47,10 @@ 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.PersistableBundle)}. This will * never be null. If you did not set any extras this will be an empty bundle. */ - public Bundle getExtras() { + public PersistableBundle getExtras() { return extras; } @@ -61,7 +61,7 @@ public class TaskParams implements Parcelable { private TaskParams(Parcel in) { taskId = in.readInt(); - extras = in.readBundle(); + extras = in.readPersistableBundle(); callback = in.readStrongBinder(); } @@ -73,7 +73,7 @@ public class TaskParams implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(taskId); - dest.writeBundle(extras); + dest.writePersistableBundle(extras); dest.writeStrongBinder(callback); } 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/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/ContentResolver.java b/core/java/android/content/ContentResolver.java index 7642e13..392bfbc 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -1839,19 +1839,6 @@ public abstract class ContentResolver { } /** - * Cancel any active or pending syncs that are running on this service. - * - * @param cname the service for which to cancel all active/pending operations. - */ - public static void cancelSync(ComponentName cname) { - try { - getContentService().cancelSync(null, null, cname); - } catch (RemoteException e) { - - } - } - - /** * Get information about the SyncAdapters that are known to the system. * @return an array of SyncAdapters that have registered with the system */ @@ -1991,13 +1978,13 @@ public abstract class ContentResolver { /** * Remove the specified sync. This will cancel any pending or active syncs. If the request is * for a periodic sync, this call will remove any future occurrences. - * <p>If a periodic sync is specified, the caller must hold the permission - * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. If this SyncRequest targets a - * SyncService adapter,the calling application must be signed with the same certificate as the - * adapter. - *</p>It is possible to cancel a sync using a SyncRequest object that is not the same object + * <p> + * If a periodic sync is specified, the caller must hold the permission + * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. + *</p> + * It is possible to cancel a sync using a SyncRequest object that is not the same object * with which you requested the sync. Do so by building a SyncRequest with the same - * service/adapter, frequency, <b>and</b> extras bundle. + * adapter, frequency, <b>and</b> extras bundle. * * @param request SyncRequest object containing information about sync to cancel. */ @@ -2031,22 +2018,6 @@ public abstract class ContentResolver { } /** - * Return periodic syncs associated with the provided component. - * <p>The calling application must be signed with the same certificate as the target component, - * otherwise this call will fail. - */ - public static List<PeriodicSync> getPeriodicSyncs(ComponentName cname) { - if (cname == null) { - throw new IllegalArgumentException("Component must not be null"); - } - try { - return getContentService().getPeriodicSyncs(null, null, cname); - } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); - } - } - - /** * Check if this account/provider is syncable. * <p>This method requires the caller to hold the permission * {@link android.Manifest.permission#READ_SYNC_SETTINGS}. @@ -2076,38 +2047,6 @@ public abstract class ContentResolver { } /** - * Set whether the provided {@link SyncService} is available to process work. - * <p>This method requires the caller to hold the permission - * {@link android.Manifest.permission#WRITE_SYNC_SETTINGS}. - * <p>The calling application must be signed with the same certificate as the target component, - * otherwise this call will fail. - */ - public static void setServiceActive(ComponentName cname, boolean active) { - try { - getContentService().setServiceActive(cname, active); - } catch (RemoteException e) { - // exception ignored; if this is thrown then it means the runtime is in the midst of - // being restarted - } - } - - /** - * Query the state of this sync service. - * <p>Set with {@link #setServiceActive(ComponentName cname, boolean active)}. - * <p>The calling application must be signed with the same certificate as the target component, - * otherwise this call will fail. - * @param cname ComponentName referring to a {@link SyncService} - * @return true if jobs will be run on this service, false otherwise. - */ - public static boolean isServiceActive(ComponentName cname) { - try { - return getContentService().isServiceActive(cname); - } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); - } - } - - /** * Gets the master auto-sync setting that applies to all the providers and accounts. * If this is false then the per-provider auto-sync setting is ignored. * <p>This method requires the caller to hold the permission @@ -2164,17 +2103,6 @@ public abstract class ContentResolver { } } - public static boolean isSyncActive(ComponentName cname) { - if (cname == null) { - throw new IllegalArgumentException("component name must not be null"); - } - try { - return getContentService().isSyncActive(null, null, cname); - } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); - } - } - /** * If a sync is active returns the information about it, otherwise returns null. * <p> @@ -2249,14 +2177,6 @@ public abstract class ContentResolver { } } - public static boolean isSyncPending(ComponentName cname) { - try { - return getContentService().isSyncPending(null, null, cname); - } catch (RemoteException e) { - throw new RuntimeException("the ContentService should always be reachable", e); - } - } - /** * Request notifications when the different aspects of the SyncManager change. The * different items that can be requested are: 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/IContentService.aidl b/core/java/android/content/IContentService.aidl index 73a76e8..373f2fb 100644 --- a/core/java/android/content/IContentService.aidl +++ b/core/java/android/content/IContentService.aidl @@ -121,19 +121,6 @@ interface IContentService { */ void setIsSyncable(in Account account, String providerName, int syncable); - /** - * Corresponds roughly to setIsSyncable(String account, String provider) for syncs that bind - * to a SyncService. - */ - void setServiceActive(in ComponentName cname, boolean active); - - /** - * Corresponds roughly to getIsSyncable(String account, String provider) for syncs that bind - * to a SyncService. - * @return 0 if this SyncService is not enabled, 1 if enabled, <0 if unknown. - */ - boolean isServiceActive(in ComponentName cname); - void setMasterSyncAutomatically(boolean flag); boolean getMasterSyncAutomatically(); 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 076f657..bd07470 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -45,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; @@ -604,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). @@ -3729,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 = @@ -7347,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); @@ -7358,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 { @@ -7373,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. * diff --git a/core/java/android/content/PeriodicSync.java b/core/java/android/content/PeriodicSync.java index 836c6f8..3efd89a 100644 --- a/core/java/android/content/PeriodicSync.java +++ b/core/java/android/content/PeriodicSync.java @@ -29,14 +29,10 @@ public class PeriodicSync implements Parcelable { public final Account account; /** The authority of the sync. Can be null. */ public final String authority; - /** The service for syncing, if this is an anonymous sync. Can be null.*/ - public final ComponentName service; /** Any extras that parameters that are to be passed to the sync adapter. */ public final Bundle extras; /** How frequently the sync should be scheduled, in seconds. Kept around for API purposes. */ public final long period; - /** Whether this periodic sync runs on a {@link SyncService}. */ - public final boolean isService; /** * How much flexibility can be taken in scheduling the sync, in seconds. * {@hide} @@ -44,16 +40,11 @@ public class PeriodicSync implements Parcelable { public final long flexTime; /** - * Creates a new PeriodicSync, copying the Bundle. SM no longer uses this ctor - kept around - * becuse it is part of the API. - * Note - even calls to the old API will not use this ctor, as - * they are given a default flex time. + * Creates a new PeriodicSync, copying the Bundle. This constructor is no longer used. */ public PeriodicSync(Account account, String authority, Bundle extras, long periodInSeconds) { this.account = account; this.authority = authority; - this.service = null; - this.isService = false; if (extras == null) { this.extras = new Bundle(); } else { @@ -71,8 +62,6 @@ public class PeriodicSync implements Parcelable { public PeriodicSync(PeriodicSync other) { this.account = other.account; this.authority = other.authority; - this.service = other.service; - this.isService = other.isService; this.extras = new Bundle(other.extras); this.period = other.period; this.flexTime = other.flexTime; @@ -86,40 +75,14 @@ public class PeriodicSync implements Parcelable { long period, long flexTime) { this.account = account; this.authority = authority; - this.service = null; - this.isService = false; - this.extras = new Bundle(extras); - this.period = period; - this.flexTime = flexTime; - } - - /** - * A PeriodicSync for a sync with a specified SyncService. - * {@hide} - */ - public PeriodicSync(ComponentName service, Bundle extras, - long period, - long flexTime) { - this.account = null; - this.authority = null; - this.service = service; - this.isService = true; this.extras = new Bundle(extras); this.period = period; this.flexTime = flexTime; } private PeriodicSync(Parcel in) { - this.isService = (in.readInt() != 0); - if (this.isService) { - this.service = in.readParcelable(null); - this.account = null; - this.authority = null; - } else { - this.account = in.readParcelable(null); - this.authority = in.readString(); - this.service = null; - } + this.account = in.readParcelable(null); + this.authority = in.readString(); this.extras = in.readBundle(); this.period = in.readLong(); this.flexTime = in.readLong(); @@ -132,13 +95,8 @@ public class PeriodicSync implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(isService ? 1 : 0); - if (account == null && authority == null) { - dest.writeParcelable(service, flags); - } else { - dest.writeParcelable(account, flags); - dest.writeString(authority); - } + dest.writeParcelable(account, flags); + dest.writeString(authority); dest.writeBundle(extras); dest.writeLong(period); dest.writeLong(flexTime); @@ -165,24 +123,14 @@ public class PeriodicSync implements Parcelable { return false; } final PeriodicSync other = (PeriodicSync) o; - if (this.isService != other.isService) { - return false; - } - boolean equal = false; - if (this.isService) { - equal = service.equals(other.service); - } else { - equal = account.equals(other.account) - && authority.equals(other.authority); - } - return equal - && period == other.period - && syncExtrasEquals(extras, other.extras); + return account.equals(other.account) + && authority.equals(other.authority) + && period == other.period + && syncExtrasEquals(extras, other.extras); } /** - * Periodic sync extra comparison function. Duplicated from - * {@link com.android.server.content.SyncManager#syncExtrasEquals(Bundle b1, Bundle b2)} + * Periodic sync extra comparison function. * {@hide} */ public static boolean syncExtrasEquals(Bundle b1, Bundle b2) { @@ -207,7 +155,6 @@ public class PeriodicSync implements Parcelable { public String toString() { return "account: " + account + ", authority: " + authority + - ", service: " + service + ". period: " + period + "s " + ", flex: " + flexTime; } diff --git a/core/java/android/content/RestrictionEntry.java b/core/java/android/content/RestrictionEntry.java index 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 6b4404d..46c9234 100644 --- a/core/java/android/content/SharedPreferences.java +++ b/core/java/android/content/SharedPreferences.java @@ -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/SyncInfo.java b/core/java/android/content/SyncInfo.java index 146dd99..a586d6f 100644 --- a/core/java/android/content/SyncInfo.java +++ b/core/java/android/content/SyncInfo.java @@ -28,24 +28,16 @@ public class SyncInfo implements Parcelable { public final int authorityId; /** - * The {@link Account} that is currently being synced. Will be null if this sync is running via - * a {@link SyncService}. + * The {@link Account} that is currently being synced. */ public final Account account; /** - * The authority of the provider that is currently being synced. Will be null if this sync - * is running via a {@link SyncService}. + * The authority of the provider that is currently being synced. */ public final String authority; /** - * The {@link SyncService} that is targeted by this operation. Null if this sync is running via - * a {@link AbstractThreadedSyncAdapter}. - */ - public final ComponentName service; - - /** * The start time of the current sync operation in milliseconds since boot. * This is represented in elapsed real time. * See {@link android.os.SystemClock#elapsedRealtime()}. @@ -53,13 +45,11 @@ public class SyncInfo implements Parcelable { public final long startTime; /** @hide */ - public SyncInfo(int authorityId, Account account, String authority, ComponentName service, - long startTime) { + public SyncInfo(int authorityId, Account account, String authority, long startTime) { this.authorityId = authorityId; this.account = account; this.authority = authority; this.startTime = startTime; - this.service = service; } /** @hide */ @@ -68,7 +58,6 @@ public class SyncInfo implements Parcelable { this.account = new Account(other.account.name, other.account.type); this.authority = other.authority; this.startTime = other.startTime; - this.service = other.service; } /** @hide */ @@ -82,8 +71,6 @@ public class SyncInfo implements Parcelable { parcel.writeParcelable(account, flags); parcel.writeString(authority); parcel.writeLong(startTime); - parcel.writeParcelable(service, flags); - } /** @hide */ @@ -92,7 +79,6 @@ public class SyncInfo implements Parcelable { account = parcel.readParcelable(Account.class.getClassLoader()); authority = parcel.readString(); startTime = parcel.readLong(); - service = parcel.readParcelable(ComponentName.class.getClassLoader()); } /** @hide */ diff --git a/core/java/android/content/SyncRequest.java b/core/java/android/content/SyncRequest.java index 9ba45ca..869f85c 100644 --- a/core/java/android/content/SyncRequest.java +++ b/core/java/android/content/SyncRequest.java @@ -27,23 +27,11 @@ public class SyncRequest implements Parcelable { private final Account mAccountToSync; /** Authority string that corresponds to a ContentProvider. */ private final String mAuthority; - /** {@link SyncService} identifier. */ - private final ComponentName mComponentInfo; /** Bundle containing user info as well as sync settings. */ private final Bundle mExtras; /** Don't allow this sync request on metered networks. */ private final boolean mDisallowMetered; /** - * Anticipated upload size in bytes. - * TODO: Not yet used - we put this information into the bundle for simplicity. - */ - private final long mTxBytes; - /** - * Anticipated download size in bytes. - * TODO: Not yet used - we put this information into the bundle. - */ - private final long mRxBytes; - /** * Amount of time before {@link #mSyncRunTimeSecs} from which the sync may optionally be * started. */ @@ -75,25 +63,12 @@ public class SyncRequest implements Parcelable { /** * {@hide} - * @return true if this sync uses an account/authority pair, or false if - * this is an anonymous sync bound to an @link AnonymousSyncService. - */ - public boolean hasAuthority() { - return mIsAuthority; - } - - /** - * {@hide} * * @return account object for this sync. * @throws IllegalArgumentException if this function is called for a request that targets a * sync service. */ public Account getAccount() { - if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getAccount() for a sync that targets a sync" - + "service."); - } return mAccountToSync; } @@ -105,30 +80,11 @@ public class SyncRequest implements Parcelable { * sync service. */ public String getProvider() { - if (!hasAuthority()) { - throw new IllegalArgumentException("Cannot getProvider() for a sync that targets a" - + "sync service."); - } return mAuthority; } /** * {@hide} - * Throws a runtime IllegalArgumentException if this function is called for a - * SyncRequest that is bound to an account/provider. - * - * @return ComponentName for the service that this sync will bind to. - */ - public ComponentName getService() { - if (hasAuthority()) { - throw new IllegalArgumentException( - "Cannot getAnonymousService() for a sync that has specified a provider."); - } - return mComponentInfo; - } - - /** - * {@hide} * Retrieve bundle for this SyncRequest. Will not be null. */ public Bundle getBundle() { @@ -175,16 +131,10 @@ public class SyncRequest implements Parcelable { parcel.writeLong(mSyncRunTimeSecs); parcel.writeInt((mIsPeriodic ? 1 : 0)); parcel.writeInt((mDisallowMetered ? 1 : 0)); - parcel.writeLong(mTxBytes); - parcel.writeLong(mRxBytes); parcel.writeInt((mIsAuthority ? 1 : 0)); parcel.writeInt((mIsExpedited? 1 : 0)); - if (mIsAuthority) { - parcel.writeParcelable(mAccountToSync, flags); - parcel.writeString(mAuthority); - } else { - parcel.writeParcelable(mComponentInfo, flags); - } + parcel.writeParcelable(mAccountToSync, flags); + parcel.writeString(mAuthority); } private SyncRequest(Parcel in) { @@ -193,19 +143,10 @@ public class SyncRequest implements Parcelable { mSyncRunTimeSecs = in.readLong(); mIsPeriodic = (in.readInt() != 0); mDisallowMetered = (in.readInt() != 0); - mTxBytes = in.readLong(); - mRxBytes = in.readLong(); mIsAuthority = (in.readInt() != 0); mIsExpedited = (in.readInt() != 0); - if (mIsAuthority) { - mComponentInfo = null; - mAccountToSync = in.readParcelable(null); - mAuthority = in.readString(); - } else { - mComponentInfo = in.readParcelable(null); - mAccountToSync = null; - mAuthority = null; - } + mAccountToSync = in.readParcelable(null); + mAuthority = in.readString(); } /** {@hide} Protected ctor to instantiate anonymous SyncRequest. */ @@ -214,7 +155,6 @@ public class SyncRequest implements Parcelable { mSyncRunTimeSecs = b.mSyncRunTimeSecs; mAccountToSync = b.mAccount; mAuthority = b.mAuthority; - mComponentInfo = b.mComponentName; mIsPeriodic = (b.mSyncType == Builder.SYNC_TYPE_PERIODIC); mIsAuthority = (b.mSyncTarget == Builder.SYNC_TARGET_ADAPTER); mIsExpedited = b.mExpedited; @@ -223,8 +163,6 @@ public class SyncRequest implements Parcelable { // TODO: pass the configuration extras through separately. mExtras.putAll(b.mSyncConfigExtras); mDisallowMetered = b.mDisallowMetered; - mTxBytes = b.mTxBytes; - mRxBytes = b.mRxBytes; } /** @@ -240,8 +178,6 @@ public class SyncRequest implements Parcelable { private static final int SYNC_TYPE_ONCE = 2; /** Unknown sync target. */ private static final int SYNC_TARGET_UNKNOWN = 0; - /** Specify that this is an anonymous sync. */ - private static final int SYNC_TARGET_SERVICE = 1; /** Specify that this is a sync with a provider. */ private static final int SYNC_TARGET_ADAPTER = 2; /** @@ -275,7 +211,7 @@ public class SyncRequest implements Parcelable { * Whether this builder is building a periodic sync, or a one-time sync. */ private int mSyncType = SYNC_TYPE_UNKNOWN; - /** Whether this will go to a sync adapter or to a sync service. */ + /** Whether this will go to a sync adapter. */ private int mSyncTarget = SYNC_TARGET_UNKNOWN; /** Whether this is a user-activated sync. */ private boolean mIsManual; @@ -298,12 +234,6 @@ public class SyncRequest implements Parcelable { private boolean mExpedited; /** - * The {@link SyncService} component that - * contains the sync logic if this is a provider-less sync, otherwise - * null. - */ - private ComponentName mComponentName; - /** * The Account object that together with an Authority name define the SyncAdapter (if * this sync is bound to a provider), otherwise null. */ @@ -336,7 +266,7 @@ public class SyncRequest implements Parcelable { /** * Build a periodic sync. Either this or syncOnce() <b>must</b> be called for this builder. - * Syncs are identified by target {@link SyncService}/{@link android.provider} and by the + * Syncs are identified by target {@link android.provider} and by the * contents of the extras bundle. * You cannot reuse the same builder for one-time syncs after having specified a periodic * sync (by calling this function). If you do, an <code>IllegalArgumentException</code> @@ -395,23 +325,10 @@ public class SyncRequest implements Parcelable { } /** - * Developer can provide insight into their payload size; optional. -1 specifies unknown, - * so that you are not restricted to defining both fields. - * - * @param rxBytes Bytes expected to be downloaded. - * @param txBytes Bytes expected to be uploaded. - */ - public Builder setTransferSize(long rxBytes, long txBytes) { - mRxBytes = rxBytes; - mTxBytes = txBytes; - return this; - } - - /** * Will throw an <code>IllegalArgumentException</code> if called and * {@link #setIgnoreSettings(boolean ignoreSettings)} has already been called. * @param disallow true to allow this transfer on metered networks. Default false. - * + * */ public Builder setDisallowMetered(boolean disallow) { if (mIgnoreSettings && disallow) { @@ -423,10 +340,9 @@ public class SyncRequest implements Parcelable { } /** - * Specify an authority and account for this transfer. Cannot be used with - * {@link #setSyncAdapter(ComponentName cname)}. + * Specify an authority and account for this transfer. * - * @param authority + * @param authority A String identifying the content provider to be synced. * @param account Account to sync. Can be null unless this is a periodic * sync, for which verification by the ContentResolver will * fail. If a sync is performed without an account, the @@ -441,25 +357,6 @@ public class SyncRequest implements Parcelable { mSyncTarget = SYNC_TARGET_ADAPTER; mAccount = account; mAuthority = authority; - mComponentName = null; - return this; - } - - /** - * Specify the {@link SyncService} component for this sync. This is not validated until - * sync time so providing an incorrect component name here will not fail. Cannot be used - * with {@link #setSyncAdapter(Account account, String authority)}. - * - * @param cname ComponentName to identify your Anonymous service - */ - public Builder setSyncAdapter(ComponentName cname) { - if (mSyncTarget != SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Sync target has already been defined."); - } - mSyncTarget = SYNC_TARGET_SERVICE; - mComponentName = cname; - mAccount = null; - mAuthority = null; return this; } @@ -630,25 +527,17 @@ public class SyncRequest implements Parcelable { mSyncConfigExtras.putInt(ContentResolver.SYNC_EXTRAS_PRIORITY, mPriority); if (mSyncType == SYNC_TYPE_PERIODIC) { // If this is a periodic sync ensure than invalid extras were not set. - if (ContentResolver.invalidPeriodicExtras(mCustomExtras) || + if (ContentResolver.invalidPeriodicExtras(mCustomExtras) || ContentResolver.invalidPeriodicExtras(mSyncConfigExtras)) { throw new IllegalArgumentException("Illegal extras were set"); } - } else if (mSyncType == SYNC_TYPE_UNKNOWN) { - throw new IllegalArgumentException("Must call either syncOnce() or syncPeriodic()"); - } - if (mSyncTarget == SYNC_TARGET_SERVICE) { - if (mSyncConfigExtras.getBoolean(ContentResolver.SYNC_EXTRAS_INITIALIZE, false)) { - throw new IllegalArgumentException("Cannot specify an initialisation sync" - + " that targets a service."); - } } // Ensure that a target for the sync has been set. if (mSyncTarget == SYNC_TARGET_UNKNOWN) { - throw new IllegalArgumentException("Must specify an adapter with one of" - + "setSyncAdapter(ComponentName) or setSyncAdapter(Account, String"); + throw new IllegalArgumentException("Must specify an adapter with" + + " setSyncAdapter(Account, String"); } return new SyncRequest(this); } - } + } } diff --git a/core/java/android/content/SyncService.java b/core/java/android/content/SyncService.java deleted file mode 100644 index 4df998c..0000000 --- a/core/java/android/content/SyncService.java +++ /dev/null @@ -1,211 +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.content; - -import android.app.Service; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Process; -import android.os.Trace; -import android.util.SparseArray; -import android.util.Log; - -import com.android.internal.annotations.GuardedBy; - -/** - * Simplified @link android.content.AbstractThreadedSyncAdapter. Folds that - * behaviour into a service to which the system can bind when requesting an - * anonymous (providerless/accountless) sync. - * <p> - * In order to perform an anonymous sync operation you must extend this service, implementing the - * abstract methods. This service must be declared in the application's manifest as usual. You - * can use this service for other work, however you <b> must not </b> override the onBind() method - * unless you know what you're doing, which limits the usefulness of this service for other work. - * <p>A {@link SyncService} can either be active or inactive. Different to an - * {@link AbstractThreadedSyncAdapter}, there is no - * {@link ContentResolver#setSyncAutomatically(android.accounts.Account account, String provider, boolean sync)}, - * as well as no concept of initialisation (you can handle your own if needed). - * - * <pre> - * <service android:name=".MySyncService"/> - * </pre> - * Like @link android.content.AbstractThreadedSyncAdapter this service supports - * multiple syncs at the same time. Each incoming startSync() with a unique tag - * will spawn a thread to do the work of that sync. If startSync() is called - * with a tag that already exists, a SyncResult.ALREADY_IN_PROGRESS is returned. - * Remember that your service will spawn multiple threads if you schedule multiple syncs - * at once, so if you mutate local objects you must ensure synchronization. - */ -public abstract class SyncService extends Service { - private static final String TAG = "SyncService"; - - private final SyncAdapterImpl mSyncAdapter = new SyncAdapterImpl(); - - /** Keep track of on-going syncs, keyed by bundle. */ - @GuardedBy("mSyncThreadLock") - private final SparseArray<SyncThread> - mSyncThreads = new SparseArray<SyncThread>(); - /** Lock object for accessing the SyncThreads HashMap. */ - private final Object mSyncThreadLock = new Object(); - /** - * Default key for if this sync service does not support parallel operations. Currently not - * sure if null keys will make it into the ArrayMap for KLP, so keeping our default for now. - */ - private static final int KEY_DEFAULT = 0; - /** Identifier for this sync service. */ - private ComponentName mServiceComponent; - - /** {@hide} */ - public IBinder onBind(Intent intent) { - mServiceComponent = new ComponentName(this, getClass()); - return mSyncAdapter.asBinder(); - } - - /** {@hide} */ - private class SyncAdapterImpl extends ISyncServiceAdapter.Stub { - @Override - public void startSync(ISyncContext syncContext, Bundle extras) { - // Wrap the provided Sync Context because it may go away by the time - // we call it. - final SyncContext syncContextClient = new SyncContext(syncContext); - boolean alreadyInProgress = false; - final int extrasAsKey = extrasToKey(extras); - synchronized (mSyncThreadLock) { - if (mSyncThreads.get(extrasAsKey) == null) { - if (Log.isLoggable(TAG, Log.VERBOSE)) { - Log.v(TAG, "starting sync for : " + mServiceComponent); - } - // Start sync. - SyncThread syncThread = new SyncThread(syncContextClient, extras); - mSyncThreads.put(extrasAsKey, syncThread); - syncThread.start(); - } else { - // Don't want to call back to SyncManager while still - // holding lock. - alreadyInProgress = true; - } - } - if (alreadyInProgress) { - syncContextClient.onFinished(SyncResult.ALREADY_IN_PROGRESS); - } - } - - /** - * Used by the SM to cancel a specific sync using the - * com.android.server.content.SyncManager.ActiveSyncContext as a handle. - */ - @Override - public void cancelSync(ISyncContext syncContext) { - SyncThread runningSync = null; - synchronized (mSyncThreadLock) { - for (int i = 0; i < mSyncThreads.size(); i++) { - SyncThread thread = mSyncThreads.valueAt(i); - if (thread.mSyncContext.getSyncContextBinder() == syncContext.asBinder()) { - runningSync = thread; - break; - } - } - } - if (runningSync != null) { - runningSync.interrupt(); - } - } - } - - /** - * - * @param extras Bundle for which to compute hash - * @return an integer hash that is equal to that of another bundle if they both contain the - * same key -> value mappings, however, not necessarily in order. - * Based on the toString() representation of the value mapped. - */ - private int extrasToKey(Bundle extras) { - int hash = KEY_DEFAULT; // Empty bundle, or no parallel operations enabled. - if (parallelSyncsEnabled()) { - for (String key : extras.keySet()) { - String mapping = key + " " + extras.get(key).toString(); - hash += mapping.hashCode(); - } - } - return hash; - } - - /** - * {@hide} - * Similar to {@link android.content.AbstractThreadedSyncAdapter.SyncThread}. However while - * the ATSA considers an already in-progress sync to be if the account provided is currently - * syncing, this anonymous sync has no notion of account and considers a sync unique if the - * provided bundle is different. - */ - private class SyncThread extends Thread { - private final SyncContext mSyncContext; - private final Bundle mExtras; - private final int mThreadsKey; - - public SyncThread(SyncContext syncContext, Bundle extras) { - mSyncContext = syncContext; - mExtras = extras; - mThreadsKey = extrasToKey(extras); - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - Trace.traceBegin(Trace.TRACE_TAG_SYNC_MANAGER, getApplication().getPackageName()); - - SyncResult syncResult = new SyncResult(); - try { - if (isCancelled()) return; - // Run the sync. - SyncService.this.onPerformSync(mExtras, syncResult); - } finally { - Trace.traceEnd(Trace.TRACE_TAG_SYNC_MANAGER); - if (!isCancelled()) { - mSyncContext.onFinished(syncResult); - } - // Synchronize so that the assignment will be seen by other - // threads that also synchronize accesses to mSyncThreads. - synchronized (mSyncThreadLock) { - mSyncThreads.remove(mThreadsKey); - } - } - } - - private boolean isCancelled() { - return Thread.currentThread().isInterrupted(); - } - } - - /** - * Initiate an anonymous sync using this service. SyncAdapter-specific - * parameters may be specified in extras, which is guaranteed to not be - * null. - */ - public abstract void onPerformSync(Bundle extras, SyncResult syncResult); - - /** - * Override this function to indicated whether you want to support parallel syncs. - * <p>If you override and return true multiple threads will be spawned within your Service to - * handle each concurrent sync request. - * - * @return false to indicate that this service does not support parallel operations by default. - */ - protected boolean parallelSyncsEnabled() { - return false; - } -} diff --git a/core/java/android/content/pm/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..a34a1b6 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,15 +513,17 @@ public abstract class PackageManager { * flag. * @hide */ + @PrivateApi public static final int INSTALL_FAILED_TEST_ONLY = -15; /** * Installation return code: this is passed to the {@link IPackageInstallObserver} by * {@link #installPackage(android.net.Uri, IPackageInstallObserver, int)} if * the package being installed contains native code, but none that is - * compatible with the the device's CPU_ABI. + * compatible with 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; /** @@ -1366,7 +1402,15 @@ public abstract class PackageManager { * The device supports managed profiles for enterprise users. */ @SdkConstant(SdkConstantType.FEATURE) - public static final String FEATURE_MANAGEDPROFILES = "android.software.managedprofiles"; + public static final String FEATURE_MANAGED_PROFILES = "android.software.managed_profiles"; + + /** + * 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. @@ -1557,7 +1601,7 @@ public abstract class PackageManager { * <p> * Throws {@link NameNotFoundException} if a package with the given name * cannot be found on the system. - * + * * @param packageName The name of the package to inspect. * @return Returns either a fully-qualified Intent that can be used to launch * the main Leanback activity in the package, or null if the package @@ -1571,7 +1615,7 @@ public abstract class PackageManager { * <p> * Throws {@link NameNotFoundException} if a package with the given name * cannot be found on the system. - * + * * @param packageName The full name (i.e. com.google.apps.contacts) of the * desired package. * @return Returns an int array of the assigned gids, or null if there are @@ -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); /** @@ -3405,7 +3454,7 @@ public abstract class PackageManager { /** - * Return the the enabled setting for a package component (activity, + * Return the enabled setting for a package component (activity, * receiver, service, provider). This returns the last value set by * {@link #setComponentEnabledSetting(ComponentName, int, int)}; in most * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since @@ -3443,14 +3492,14 @@ public abstract class PackageManager { int newState, int flags); /** - * Return the the enabled setting for an application. This returns + * Return the enabled setting for an application. This returns * the last value set by * {@link #setApplicationEnabledSetting(String, int, int)}; in most * cases this value will be {@link #COMPONENT_ENABLED_STATE_DEFAULT} since * the value originally specified in the manifest has not been modified. * - * @param packageName The component to retrieve. - * @return Returns the current enabled state for the component. May + * @param packageName The package name of the application to retrieve. + * @return Returns the current enabled state for the application. May * be one of {@link #COMPONENT_ENABLED_STATE_ENABLED}, * {@link #COMPONENT_ENABLED_STATE_DISABLED}, or * {@link #COMPONENT_ENABLED_STATE_DEFAULT}. The last one means the @@ -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..9625578 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) { @@ -2228,9 +2239,7 @@ public class Resources { } // First, check whether we have a cached version of this drawable - // that's valid for the specified theme. This may apply a theme to a - // cached drawable that has themeable attributes but was not previously - // themed. + // that was inflated against the specified theme. if (!mPreloading) { final Drawable cachedDrawable = getCachedDrawable(caches, key, theme); if (cachedDrawable != null) { @@ -2256,8 +2265,8 @@ public class Resources { dr = loadDrawableForCookie(value, id, theme); } - // If we were able to obtain a drawable, attempt to place it in the - // appropriate cache (e.g. no theme, themed, themeable). + // If we were able to obtain a drawable, store it in the appropriate + // cache (either preload or themed). if (dr != null) { dr.setChangingConfigurations(value.changingConfigurations); cacheDrawable(value, theme, isColorDrawable, caches, key, dr); @@ -2267,7 +2276,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 +2306,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 +2350,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,9 +2370,11 @@ 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 String themeKey = theme != null ? theme.mKey : ""; final LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey); if (themedCache != null) { final Drawable themedDrawable = getCachedDrawableLocked(themedCache, key); @@ -2593,21 +2609,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 86208fc..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,321 @@ 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. @@ -410,6 +734,7 @@ public final class Sensor { * 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; @@ -467,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) { @@ -525,6 +872,8 @@ public final class Sensor { private int mFifoMaxEventCount; private String mStringType; private String mRequiredPermission; + private int mMaxDelay; + private boolean mWakeUpSensor; Sensor() { } @@ -613,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() { @@ -624,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..5fd0f9b --- /dev/null +++ b/core/java/android/hardware/camera2/CameraCaptureSession.java @@ -0,0 +1,690 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 the session + * was explicitly closed, 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 + * @see #abortCaptures + */ + 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 the session + * was explicitly closed, 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 + * @see #abortCaptures + */ + 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 the session + * was explicitly closed, 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. + * Or if no requests were passed in. + * + * @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 the session + * was explicitly closed, 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. Or if no + * requests were passed in. + * + * @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 the session + * was explicitly closed, 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 the session + * was explicitly closed, 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> + * + * <p>Closing a session is idempotent; closing more than once has no effect.</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> + * + * @param session the session returned by {@link CameraDevice#createCaptureSession} + */ + 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> + * + * @param session the session returned by {@link CameraDevice#createCaptureSession} + */ + 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> + * + * @param session the session returned by {@link CameraDevice#createCaptureSession} + * + */ + 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> + * + * @param session the session returned by {@link CameraDevice#createCaptureSession} + */ + 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> + * + * @param session the session returned by {@link CameraDevice#createCaptureSession} + */ + 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 session the session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session the session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session the session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session the session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session + * The session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session + * The session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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 session + * The session returned by {@link CameraDevice#createCaptureSession} + * @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(CameraCaptureSession session, + 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..08cfc87 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,91 @@ 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 + * @hide */ public static final Key<Integer> REQUEST_MAX_NUM_INPUT_STREAMS = new Key<Integer>("android.request.maxNumInputStreams", int.class); @@ -550,7 +811,7 @@ 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> + * <li>MANUAL_POST_PROCESSING</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 +820,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 +854,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 +865,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 +889,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 +918,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 +936,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,15 +964,18 @@ 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 * camera device for input streams, to their corresponding output formats.</p> * <p>All camera devices with at least 1 - * {@link CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS android.request.maxNumInputStreams} will have at least one + * android.request.maxNumInputStreams will have at least one * available input format.</p> * <p>The camera device will support the following map of formats, * if its dependent capability is supported:</p> @@ -746,13 +1017,12 @@ 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> - * - * @see CameraCharacteristics#REQUEST_MAX_NUM_INPUT_STREAMS - * @see CameraCharacteristics#SCALER_AVAILABLE_STALL_DURATIONS + * <p>TODO: typedef to ReprocessFormatMap</p> + * @hide */ public static final Key<int[]> SCALER_AVAILABLE_INPUT_OUTPUT_FORMATS_MAP = new Key<int[]>("android.scaler.availableInputOutputFormatsMap", int[].class); @@ -763,8 +1033,6 @@ public final class CameraCharacteristics extends CameraMetadata { * (i.e. format, width, height, output/input stream).</p> * <p>The configurations are listed as <code>(format, width, height, input?)</code> * tuples.</p> - * <p>All camera devices will support sensor maximum resolution (defined by - * {@link CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE android.sensor.info.activeArraySize}) for the JPEG format.</p> * <p>For a given use case, the actual maximum supported resolution * may be lower than what is listed here, depending on the destination * Surface for the image data. For example, for recording video, @@ -775,7 +1043,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 +1112,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 +1129,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 +1197,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 +1328,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 +1350,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 +1362,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 +1375,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 +1482,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 +1503,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 +1525,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 +1549,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 +1569,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 +1591,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 +1659,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 +1683,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 +1705,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 +1722,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..e9213c5 100644 --- a/core/java/android/hardware/camera2/CameraDevice.java +++ b/core/java/android/hardware/camera2/CameraDevice.java @@ -16,13 +16,15 @@ package android.hardware.camera2; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.graphics.ImageFormat; import android.os.Handler; import android.view.Surface; import java.util.List; /** - * <p>The CameraDevice class is an interface to a single camera connected to an + * <p>The CameraDevice class is a representation of a single camera connected to an * Android device, allowing for fine-grain control of image capture and * post-processing at high frame rates.</p> * @@ -44,7 +46,7 @@ import java.util.List; * @see CameraManager#openCamera * @see android.Manifest.permission#CAMERA */ -public interface CameraDevice extends AutoCloseable { +public abstract class CameraDevice implements AutoCloseable { /** * Create a request suitable for a camera preview window. Specifically, this @@ -125,7 +127,7 @@ public interface CameraDevice extends AutoCloseable { * @see CameraManager#getCameraCharacteristics * @see CameraManager#getCameraIdList */ - public String getId(); + public abstract String getId(); /** * <p>Set up a new output set of Surfaces for the camera device.</p> @@ -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,8 +242,125 @@ 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; + @Deprecated + public abstract 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 abstract void createCaptureSession(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler handler) + throws CameraAccessException; /** * <p>Create a {@link CaptureRequest.Builder} for new capture requests, @@ -271,7 +387,7 @@ public interface CameraDevice extends AutoCloseable { * @see #TEMPLATE_VIDEO_SNAPSHOT * @see #TEMPLATE_MANUAL */ - public CaptureRequest.Builder createCaptureRequest(int templateType) + public abstract CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException; /** @@ -315,8 +431,10 @@ 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) + @Deprecated + public abstract int capture(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; /** @@ -354,13 +472,16 @@ public interface CameraDevice extends AutoCloseable { * 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. + * is not null, and the calling thread has no looper. Or if no requests were + * passed in. * * @see #capture * @see #setRepeatingRequest * @see #setRepeatingBurst + * @deprecated Use {@link CameraCaptureSession} instead */ - public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, + @Deprecated + public abstract int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; /** @@ -417,8 +538,10 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingBurst * @see #stopRepeating * @see #flush + * @deprecated Use {@link CameraCaptureSession} instead */ - public int setRepeatingRequest(CaptureRequest request, CaptureListener listener, + @Deprecated + public abstract int setRepeatingRequest(CaptureRequest request, CaptureListener listener, Handler handler) throws CameraAccessException; /** @@ -468,15 +591,18 @@ public interface CameraDevice extends AutoCloseable { * 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. + * is not null, and the calling thread has no looper. Or if no requests were + * passed in. * * @see #capture * @see #captureBurst * @see #setRepeatingRequest * @see #stopRepeating * @see #flush + * @deprecated Use {@link CameraCaptureSession} instead */ - public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, + @Deprecated + public abstract int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException; /** @@ -499,8 +625,10 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingRequest * @see #setRepeatingBurst * @see StateListener#onIdle + * @deprecated Use {@link CameraCaptureSession} instead */ - public void stopRepeating() throws CameraAccessException; + @Deprecated + public abstract void stopRepeating() throws CameraAccessException; /** * Flush all captures currently pending and in-progress as fast as @@ -535,28 +663,28 @@ public interface CameraDevice extends AutoCloseable { * @see #setRepeatingRequest * @see #setRepeatingBurst * @see #configureOutputs + * @deprecated Use {@link CameraCaptureSession} instead */ - public void flush() throws CameraAccessException; + @Deprecated + public abstract 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(); + public abstract void close(); /** * <p>A listener for tracking the progress of a {@link CaptureRequest} @@ -570,7 +698,9 @@ public interface CameraDevice extends AutoCloseable { * @see #captureBurst * @see #setRepeatingRequest * @see #setRepeatingBurst + * @deprecated Use {@link CameraCaptureSession} instead */ + @Deprecated public static abstract class CaptureListener { /** @@ -646,14 +776,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 +841,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 +854,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 +883,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,7 +1050,9 @@ 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. */ + @Deprecated public void onUnconfigured(CameraDevice camera) { // Default empty implementation } @@ -864,7 +1081,9 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#captureBurst * @see CameraDevice#setRepeatingBurst * @see CameraDevice#setRepeatingRequest + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onActive(CameraDevice camera) { // Default empty implementation } @@ -897,7 +1116,9 @@ public interface CameraDevice extends AutoCloseable { * * @see CameraDevice#configureOutputs * @see CameraDevice#flush + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onBusy(CameraDevice camera) { // Default empty implementation } @@ -944,7 +1165,9 @@ public interface CameraDevice extends AutoCloseable { * @see CameraDevice#configureOutputs * @see CameraDevice#stopRepeating * @see CameraDevice#flush + * @deprecated Use {@link CameraCaptureSession.StateListener} instead. */ + @Deprecated public void onIdle(CameraDevice camera) { // Default empty implementation } @@ -1007,4 +1230,10 @@ public interface CameraDevice extends AutoCloseable { */ public abstract void onError(CameraDevice camera, int error); // Must implement } + + /** + * To be inherited by android.hardware.camera2.* code only. + * @hide + */ + public CameraDevice() {} } diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java index 0fcd598..7c0f37e 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,68 @@ 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; + ICameraDeviceUser cameraUser = null; - android.hardware.camera2.impl.CameraDevice device = - new android.hardware.camera2.impl.CameraDevice( + android.hardware.camera2.impl.CameraDeviceImpl deviceImpl = + new android.hardware.camera2.impl.CameraDeviceImpl( 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 if (e.getReason() == CameraAccessException.CAMERA_IN_USE || + e.getReason() == CameraAccessException.MAX_CAMERAS_IN_USE || + e.getReason() == CameraAccessException.CAMERA_DISABLED || + e.getReason() == CameraAccessException.CAMERA_DISCONNECTED || + e.getReason() == CameraAccessException.CAMERA_ERROR) { + // Received one of the known connection errors + // The remote camera device cannot be connected to, so + // set the local camera to the startup error state + deviceImpl.setRemoteFailure(e); + + if (e.getReason() == CameraAccessException.CAMERA_DISABLED || + e.getReason() == CameraAccessException.CAMERA_DISCONNECTED) { + // Per API docs, these failures call onError and throw + throw e; + } + } else { + // Unexpected failure - rethrow + 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 +283,7 @@ public final class CameraManager { } catch (RemoteException e) { // impossible } + return device; } /** @@ -265,20 +294,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 a {@link CameraAccessException}.</p> * * @param cameraId * The unique identifier of the camera device to open @@ -405,6 +440,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 6659278..94a5a79 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -16,8 +16,7 @@ package android.hardware.camera2; -import android.hardware.camera2.impl.CameraMetadataNative; -import android.hardware.camera2.utils.TypeReference; +import android.util.Log; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -36,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> * @@ -44,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 @@ -74,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. @@ -83,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)); } /** @@ -101,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); } } @@ -127,113 +145,6 @@ public abstract class CameraMetadata { return keyList; } - // TODO: make final or abstract - 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; - - /** - * @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; - mTypeReference = TypeReference.createSpecializedTypeReference(type); - } - - /** - * @hide - */ - @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; - } - - public final String getName() { - return mName; - } - - @Override - public final int hashCode() { - return mName.hashCode() ^ mTypeReference.hashCode(); - } - - @Override - 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) && 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 - * - * @hide - */ - 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> - * - * @hide - */ - 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> - * - * @hide - */ - public final TypeReference<T> getTypeReference() { - return mTypeReference; - } - } - /*@O~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~ * The enum values below this point are generated from metadata * definitions in /system/media/camera/docs. Do not modify by hand or @@ -325,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> @@ -336,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> @@ -355,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 @@ -367,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> @@ -383,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> @@ -392,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> @@ -420,6 +340,7 @@ public abstract class CameraMetadata { * (both input/output) will match the maximum available * resolution of JPEG streams.</li> * </ul> + * <p>@hide this, TODO: remove it when input related APIs are ready.</p> * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES */ public static final int REQUEST_AVAILABLE_CAPABILITIES_ZSL = 4; @@ -446,18 +367,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 @@ -1379,8 +1302,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; @@ -1638,17 +1560,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 0ca9161..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 @@ -541,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 @@ -570,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 @@ -616,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> @@ -702,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, @@ -916,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); @@ -925,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); @@ -932,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); @@ -964,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 @@ -1109,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 @@ -1185,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 @@ -1195,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 @@ -1203,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> @@ -1214,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); @@ -1275,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, @@ -1288,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 @@ -1333,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 */ @@ -1347,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); @@ -1359,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); @@ -1372,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 @@ -1388,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, @@ -1404,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, @@ -1412,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 @@ -1435,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 42a3de8..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 @@ -358,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 @@ -387,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 @@ -631,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> @@ -1068,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 @@ -1125,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, @@ -1486,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); @@ -1495,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); @@ -1502,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); @@ -1534,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 @@ -1623,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) @@ -1713,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); @@ -1758,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 @@ -1834,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 @@ -1844,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 @@ -1852,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> @@ -1863,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); @@ -1892,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 @@ -1999,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, @@ -2012,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 @@ -2079,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> @@ -2097,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, @@ -2121,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); @@ -2141,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); @@ -2163,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); @@ -2218,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 */ @@ -2239,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); @@ -2251,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); @@ -2264,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 @@ -2280,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, @@ -2296,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, @@ -2304,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 @@ -2327,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 @@ -2440,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..3e3303c --- /dev/null +++ b/core/java/android/hardware/camera2/DngCreator.java @@ -0,0 +1,406 @@ +/* + * 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.os.SystemClock; +import android.util.Size; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +/** + * 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 { + + private static final String TAG = "DngCreator"; + /** + * 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"); + } + + // Find current time + long currentTime = System.currentTimeMillis(); + + // Find boot time + long bootTimeMillis = currentTime - SystemClock.elapsedRealtime(); + + // Find capture time (nanos since boot) + Long timestamp = metadata.get(CaptureResult.SENSOR_TIMESTAMP); + long captureTime = currentTime; + if (timestamp != null) { + captureTime = timestamp / 1000000 + bootTimeMillis; + } + + // Format for metadata + String formattedCaptureTime = sDateTimeStampFormat.format(captureTime); + + nativeInit(characteristics.getNativeCopy(), metadata.getNativeCopy(), + formattedCaptureTime); + } + + /** + * 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. + * @hide + */ + 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. + * @hide + */ + 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. + * @hide + */ + 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. + * @hide + */ + 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. + * @hide + */ + 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 + * @hide + */ + 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. + * @hide + */ + 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(); + } + } + + private static final String TIFF_DATETIME_FORMAT = "yyyy:MM:dd kk:mm:ss"; + private static final DateFormat sDateTimeStampFormat = + new SimpleDateFormat(TIFF_DATETIME_FORMAT); + + static { + sDateTimeStampFormat.setTimeZone(TimeZone.getDefault()); + } + /** + * 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, + String captureTime); + + 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 5ddd7d6..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.utils.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/dispatch/ArgumentReplacingDispatcher.java b/core/java/android/hardware/camera2/dispatch/ArgumentReplacingDispatcher.java new file mode 100644 index 0000000..866f370 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/ArgumentReplacingDispatcher.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + +/** + * A dispatcher that replaces one argument with another; replaces any argument at an index + * with another argument. + * + * <p>For example, we can override an {@code void onSomething(int x)} calls to have {@code x} always + * equal to 1. Or, if using this with a duck typing dispatcher, we could even overwrite {@code x} to + * be something + * that's not an {@code int}.</p> + * + * @param <T> + * source dispatch type, whose methods with {@link #dispatch} will be called + * @param <TArg> + * argument replacement type, args in {@link #dispatch} matching {@code argumentIndex} + * will be overriden to objects of this type + */ +public class ArgumentReplacingDispatcher<T, TArg> implements Dispatchable<T> { + + private final Dispatchable<T> mTarget; + private final int mArgumentIndex; + private final TArg mReplaceWith; + + /** + * Create a new argument replacing dispatcher; dispatches are forwarded to {@code target} + * after the argument is replaced. + * + * <p>For example, if a method {@code onAction(T1 a, Integer b, T2 c)} is invoked, and we wanted + * to replace all occurrences of {@code b} with {@code 0xDEADBEEF}, we would set + * {@code argumentIndex = 1} and {@code replaceWith = 0xDEADBEEF}.</p> + * + * <p>If a method dispatched has less arguments than {@code argumentIndex}, it is + * passed through with the arguments unchanged.</p> + * + * @param target destination dispatch type, methods will be redirected to this dispatcher + * @param argumentIndex the numeric index of the argument {@code >= 0} + * @param replaceWith arguments matching {@code argumentIndex} will be replaced with this object + */ + public ArgumentReplacingDispatcher(Dispatchable<T> target, int argumentIndex, + TArg replaceWith) { + mTarget = checkNotNull(target, "target must not be null"); + mArgumentIndex = checkArgumentNonnegative(argumentIndex, + "argumentIndex must not be negative"); + mReplaceWith = checkNotNull(replaceWith, "replaceWith must not be null"); + } + + @Override + public Object dispatch(Method method, Object[] args) throws Throwable { + + if (args.length > mArgumentIndex) { + args = arrayCopy(args); // don't change in-place since it can affect upstream dispatches + args[mArgumentIndex] = mReplaceWith; + } + + return mTarget.dispatch(method, args); + } + + private static Object[] arrayCopy(Object[] array) { + int length = array.length; + Object[] newArray = new Object[length]; + for (int i = 0; i < length; ++i) { + newArray[i] = array[i]; + } + return newArray; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java new file mode 100644 index 0000000..fe575b2 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/BroadcastDispatcher.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import static com.android.internal.util.Preconditions.*; + +/** + * Broadcast a single dispatch into multiple other dispatchables. + * + * <p>Every time {@link #dispatch} is invoked, all the broadcast targets will + * see the same dispatch as well. The first target's return value is returned.</p> + * + * <p>This enables a single listener to be converted into a multi-listener.</p> + */ +public class BroadcastDispatcher<T> implements Dispatchable<T> { + + private final List<Dispatchable<T>> mDispatchTargets; + + /** + * Create a broadcast dispatcher from the supplied dispatch targets. + * + * @param dispatchTargets one or more targets to dispatch to + */ + @SafeVarargs + public BroadcastDispatcher(Dispatchable<T>... dispatchTargets) { + mDispatchTargets = Arrays.asList( + checkNotNull(dispatchTargets, "dispatchTargets must not be null")); + } + + @Override + public Object dispatch(Method method, Object[] args) throws Throwable { + Object result = null; + boolean gotResult = false; + + for (Dispatchable<T> dispatchTarget : mDispatchTargets) { + Object localResult = dispatchTarget.dispatch(method, args); + + if (!gotResult) { + gotResult = true; + result = localResult; + } + } + + return result; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/Dispatchable.java b/core/java/android/hardware/camera2/dispatch/Dispatchable.java new file mode 100644 index 0000000..753103f --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/Dispatchable.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + +import java.lang.reflect.Method; + +/** + * Dynamically dispatch a method and its argument to some object. + * + * <p>This can be used to intercept method calls and do work around them, redirect work, + * or block calls entirely.</p> + */ +public interface Dispatchable<T> { + /** + * Dispatch the method and arguments to this object. + * @param method a method defined in class {@code T} + * @param args arguments corresponding to said {@code method} + * @return the object returned when invoking {@code method} + * @throws Throwable any exception that might have been raised while invoking the method + */ + public Object dispatch(Method method, Object[] args) throws Throwable; +} diff --git a/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java new file mode 100644 index 0000000..75f97e4 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/DuckTypingDispatcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + + +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + +/** + * Duck typing dispatcher; converts dispatch methods calls from one class to another by + * looking up equivalently methods at runtime by name. + * + * <p>For example, if two types have identical method names and arguments, but + * are not subclasses/subinterfaces of each other, this dispatcher will allow calls to be + * made from one type to the other.</p> + * + * @param <TFrom> source dispatch type, whose methods with {@link #dispatch} will be called + * @param <T> destination dispatch type, methods will be converted to the class of {@code T} + */ +public class DuckTypingDispatcher<TFrom, T> implements Dispatchable<TFrom> { + + private final MethodNameInvoker<T> mDuck; + + /** + * Create a new duck typing dispatcher. + * + * @param target destination dispatch type, methods will be redirected to this dispatcher + * @param targetClass destination dispatch class, methods will be converted to this class's + */ + public DuckTypingDispatcher(Dispatchable<T> target, Class<T> targetClass) { + checkNotNull(targetClass, "targetClass must not be null"); + checkNotNull(target, "target must not be null"); + + mDuck = new MethodNameInvoker<T>(target, targetClass); + } + + @Override + public Object dispatch(Method method, Object[] args) { + return mDuck.invoke(method.getName(), args); + } +} diff --git a/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java new file mode 100644 index 0000000..f8e9d49 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/HandlerDispatcher.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; +import android.os.Handler; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + +/** + * Forward all interface calls into a handler by posting it as a {@code Runnable}. + * + * <p>All calls will return immediately; functions with return values will return a default + * value of {@code null}, {@code 0}, or {@code false} where that value is legal.</p> + * + * <p>Any exceptions thrown on the handler while trying to invoke a method + * will be re-thrown. Throwing checked exceptions on a handler which doesn't expect any + * checked exceptions to be thrown will result in "undefined" behavior + * (although in practice it is usually thrown as normal).</p> + */ +public class HandlerDispatcher<T> implements Dispatchable<T> { + + private static final String TAG = "HandlerDispatcher"; + + private final Dispatchable<T> mDispatchTarget; + private final Handler mHandler; + + /** + * Create a dispatcher that forwards it's dispatch calls by posting + * them onto the {@code handler} as a {@code Runnable}. + * + * @param dispatchTarget the destination whose method calls will be redirected into the handler + * @param handler all calls into {@code dispatchTarget} will be posted onto this handler + * @param <T> the type of the element you want to wrap. + * @return a dispatcher that will forward it's dispatch calls to a handler + */ + public HandlerDispatcher(Dispatchable<T> dispatchTarget, Handler handler) { + mDispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mHandler = checkNotNull(handler, "handler must not be null"); + } + + @Override + public Object dispatch(final Method method, final Object[] args) throws Throwable { + mHandler.post(new Runnable() { + @Override + public void run() { + try { + mDispatchTarget.dispatch(method, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + // Potential UB. Hopefully 't' is a runtime exception. + UncheckedThrow.throwAnyException(t); + } catch (IllegalAccessException e) { + // Impossible + Log.wtf(TAG, "IllegalAccessException while invoking " + method, e); + } catch (IllegalArgumentException e) { + // Impossible + Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e); + } catch (Throwable e) { + UncheckedThrow.throwAnyException(e); + } + } + }); + + // TODO handle primitive return values that would avoid NPE if unboxed + return null; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java new file mode 100644 index 0000000..ac5f526 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/InvokeDispatcher.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import static com.android.internal.util.Preconditions.*; + + +public class InvokeDispatcher<T> implements Dispatchable<T> { + + private static final String TAG = "InvocationSink"; + private final T mTarget; + + public InvokeDispatcher(T target) { + mTarget = checkNotNull(target, "target must not be null"); + } + + @Override + public Object dispatch(Method method, Object[] args) { + try { + return method.invoke(mTarget, args); + } catch (InvocationTargetException e) { + Throwable t = e.getTargetException(); + // Potential UB. Hopefully 't' is a runtime exception. + UncheckedThrow.throwAnyException(t); + } catch (IllegalAccessException e) { + // Impossible + Log.wtf(TAG, "IllegalAccessException while invoking " + method, e); + } catch (IllegalArgumentException e) { + // Impossible + Log.wtf(TAG, "IllegalArgumentException while invoking " + method, e); + } + + // unreachable + return null; + } +} diff --git a/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java new file mode 100644 index 0000000..02c3d87 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/MethodNameInvoker.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.dispatch; + +import android.hardware.camera2.utils.UncheckedThrow; + +import java.lang.reflect.Method; +import java.util.concurrent.ConcurrentHashMap; + +import static com.android.internal.util.Preconditions.*; + +/** + * Invoke a method on a dispatchable by its name (without knowing the {@code Method} ahead of time). + * + * @param <T> destination dispatch type, methods will be looked up in the class of {@code T} + */ +public class MethodNameInvoker<T> { + + private final Dispatchable<T> mTarget; + private final Class<T> mTargetClass; + private final ConcurrentHashMap<String, Method> mMethods = + new ConcurrentHashMap<>(); + + /** + * Create a new method name invoker. + * + * @param target destination dispatch type, invokes will be redirected to this dispatcher + * @param targetClass destination dispatch class, the invoked methods will be from this class + */ + public MethodNameInvoker(Dispatchable<T> target, Class<T> targetClass) { + mTargetClass = targetClass; + mTarget = target; + } + + /** + * Invoke a method by its name. + * + * <p>If more than one method exists in {@code targetClass}, the first method will be used.</p> + * + * @param methodName + * The name of the method, which will be matched 1:1 to the destination method + * @param params + * Variadic parameter list. + * @return + * The same kind of value that would normally be returned by calling {@code methodName} + * statically. + * + * @throws IllegalArgumentException if {@code methodName} does not exist on the target class + * @throws Throwable will rethrow anything that the target method would normally throw + */ + @SuppressWarnings("unchecked") + public <K> K invoke(String methodName, Object... params) { + checkNotNull(methodName, "methodName must not be null"); + + Method targetMethod = mMethods.get(methodName); + if (targetMethod == null) { + for (Method method : mTargetClass.getMethods()) { + // TODO future: match by # of params and types of params if possible + if (method.getName().equals(methodName)) { + targetMethod = method; + mMethods.put(methodName, targetMethod); + break; + } + } + + if (targetMethod == null) { + throw new IllegalArgumentException( + "Method " + methodName + " does not exist on class " + mTargetClass); + } + } + + try { + return (K) mTarget.dispatch(targetMethod, params); + } catch (Throwable e) { + UncheckedThrow.throwAnyException(e); + // unreachable + return null; + } + } +} diff --git a/core/java/android/tv/ITvInputService.aidl b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java index 4f1bc2b..fada075 100644 --- a/core/java/android/tv/ITvInputService.aidl +++ b/core/java/android/hardware/camera2/dispatch/NullDispatcher.java @@ -13,19 +13,26 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +package android.hardware.camera2.dispatch; -package android.tv; -import android.tv.ITvInputServiceCallback; -import android.tv.ITvInputSessionCallback; -import android.view.InputChannel; +import java.lang.reflect.Method; /** - * Top-level interface to a TV input component (implemented in a Service). - * @hide + * Do nothing when dispatching; follows the null object pattern. */ -oneway interface ITvInputService { - void registerCallback(ITvInputServiceCallback callback); - void unregisterCallback(in ITvInputServiceCallback callback); - void createSession(in InputChannel channel, ITvInputSessionCallback callback); +public class NullDispatcher<T> implements Dispatchable<T> { + /** + * Create a dispatcher that does nothing when dispatched to. + */ + public NullDispatcher() { + } + + /** + * Do nothing; all parameters are ignored. + */ + @Override + public Object dispatch(Method method, Object[] args) { + return null; + } } diff --git a/core/java/android/hardware/camera2/dispatch/package.html b/core/java/android/hardware/camera2/dispatch/package.html new file mode 100644 index 0000000..783d0a1 --- /dev/null +++ b/core/java/android/hardware/camera2/dispatch/package.html @@ -0,0 +1,3 @@ +<body> +{@hide} +</body> diff --git a/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java new file mode 100644 index 0000000..c3e042e --- /dev/null +++ b/core/java/android/hardware/camera2/impl/CameraCaptureSessionImpl.java @@ -0,0 +1,584 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.dispatch.ArgumentReplacingDispatcher; +import android.hardware.camera2.dispatch.BroadcastDispatcher; +import android.hardware.camera2.dispatch.Dispatchable; +import android.hardware.camera2.dispatch.DuckTypingDispatcher; +import android.hardware.camera2.dispatch.HandlerDispatcher; +import android.hardware.camera2.dispatch.InvokeDispatcher; +import android.hardware.camera2.dispatch.NullDispatcher; +import android.hardware.camera2.utils.TaskDrainer; +import android.hardware.camera2.utils.TaskSingleDrainer; +import android.os.Handler; +import android.util.Log; +import android.view.Surface; + +import java.util.Arrays; +import java.util.List; + +import static android.hardware.camera2.impl.CameraDeviceImpl.checkHandler; +import static com.android.internal.util.Preconditions.*; + +public class CameraCaptureSessionImpl extends CameraCaptureSession { + private static final String TAG = "CameraCaptureSession"; + private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE); + + /** User-specified set of surfaces used as the configuration outputs */ + private final List<Surface> mOutputs; + /** + * User-specified state listener, used for outgoing events; calls to this object will be + * automatically {@link Handler#post(Runnable) posted} to {@code mStateHandler}. + */ + private final CameraCaptureSession.StateListener mStateListener; + /** User-specified state handler used for outgoing state listener events */ + private final Handler mStateHandler; + + /** Internal camera device; used to translate calls into existing deprecated API */ + private final android.hardware.camera2.impl.CameraDeviceImpl mDeviceImpl; + /** Internal handler; used for all incoming events to preserve total order */ + private final Handler mDeviceHandler; + + /** Drain Sequence IDs which have been queued but not yet finished with aborted/completed */ + private final TaskDrainer<Integer> mSequenceDrainer; + /** Drain state transitions from ACTIVE -> IDLE */ + private final TaskSingleDrainer mIdleDrainer; + /** Drain state transitions from BUSY -> IDLE */ + private final TaskSingleDrainer mAbortDrainer; + /** Drain the UNCONFIGURED state transition */ + private final TaskSingleDrainer mUnconfigureDrainer; + + /** This session is closed; all further calls will throw ISE */ + private boolean mClosed = false; + /** Do not unconfigure if this is set; another session will overwrite configuration */ + private boolean mSkipUnconfigure = false; + + /** Is the session in the process of aborting? Pay attention to BUSY->IDLE transitions. */ + private boolean mAborting; + + /** + * Create a new CameraCaptureSession. + * + * <p>The camera device must already be in the {@code IDLE} state when this is invoked. + * There must be no pending actions + * (e.g. no pending captures, no repeating requests, no flush).</p> + */ + CameraCaptureSessionImpl(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler stateHandler, + android.hardware.camera2.impl.CameraDeviceImpl deviceImpl, + Handler deviceStateHandler, boolean configureSuccess) { + if (outputs == null || outputs.isEmpty()) { + throw new IllegalArgumentException("outputs must be a non-null, non-empty list"); + } else if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + // TODO: extra verification of outputs + mOutputs = outputs; + mStateHandler = checkHandler(stateHandler); + mStateListener = createUserStateListenerProxy(mStateHandler, listener); + + mDeviceHandler = checkNotNull(deviceStateHandler, "deviceStateHandler must not be null"); + mDeviceImpl = checkNotNull(deviceImpl, "deviceImpl must not be null"); + + /* + * Use the same handler as the device's StateListener for all the internal coming events + * + * This ensures total ordering between CameraDevice.StateListener and + * CameraDevice.CaptureListener events. + */ + mSequenceDrainer = new TaskDrainer<>(mDeviceHandler, new SequenceDrainListener(), + /*name*/"seq"); + mIdleDrainer = new TaskSingleDrainer(mDeviceHandler, new IdleDrainListener(), + /*name*/"idle"); + mAbortDrainer = new TaskSingleDrainer(mDeviceHandler, new AbortDrainListener(), + /*name*/"abort"); + mUnconfigureDrainer = new TaskSingleDrainer(mDeviceHandler, new UnconfigureDrainListener(), + /*name*/"unconf"); + + // CameraDevice should call configureOutputs and have it finish before constructing us + + if (configureSuccess) { + mStateListener.onConfigured(this); + } else { + mStateListener.onConfigureFailed(this); + mClosed = true; // do not fire any other callbacks, do not allow any other work + } + } + + @Override + public CameraDevice getDevice() { + return mDeviceImpl; + } + + @Override + public synchronized int capture(CaptureRequest request, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + Log.v(TAG, "capture - request " + request + ", listener " + listener + " handler" + + "" + handler); + } + + return addPendingSequence(mDeviceImpl.capture(request, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int captureBurst(List<CaptureRequest> requests, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]); + Log.v(TAG, "captureBurst - requests " + Arrays.toString(requestArray) + ", listener " + + listener + " handler" + "" + handler); + } + + return addPendingSequence(mDeviceImpl.captureBurst(requests, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int setRepeatingRequest(CaptureRequest request, CaptureListener listener, + Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + return addPendingSequence(mDeviceImpl.setRepeatingRequest(request, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized int setRepeatingBurst(List<CaptureRequest> requests, + CaptureListener listener, Handler handler) throws CameraAccessException { + checkNotClosed(); + checkLegalToCapture(); + + handler = checkHandler(handler); + + if (VERBOSE) { + CaptureRequest[] requestArray = requests.toArray(new CaptureRequest[0]); + Log.v(TAG, "setRepeatingBurst - requests " + Arrays.toString(requestArray) + + ", listener " + listener + " handler" + "" + handler); + } + + return addPendingSequence(mDeviceImpl.setRepeatingBurst(requests, + createCaptureListenerProxy(handler, listener), mDeviceHandler)); + } + + @Override + public synchronized void stopRepeating() throws CameraAccessException { + checkNotClosed(); + + if (VERBOSE) { + Log.v(TAG, "stopRepeating"); + } + + mDeviceImpl.stopRepeating(); + } + + @Override + public synchronized void abortCaptures() throws CameraAccessException { + checkNotClosed(); + + if (VERBOSE) { + Log.v(TAG, "abortCaptures"); + } + + if (mAborting) { + Log.w(TAG, "abortCaptures - Session is already aborting; doing nothing"); + return; + } + + mAborting = true; + mAbortDrainer.taskStarted(); + + mDeviceImpl.flush(); + // The next BUSY -> IDLE set of transitions will mark the end of the abort. + } + + /** + * Replace this session with another session. + * + * <p>This is an optimization to avoid unconfiguring and then immediately having to + * reconfigure again.</p> + * + * <p>The semantics are identical to {@link #close}, except that unconfiguring will be skipped. + * <p> + * + * @see CameraCaptureSession#close + */ + synchronized void replaceSessionClose(CameraCaptureSession other) { + /* + * In order for creating new sessions to be fast, the new session should be created + * before the old session is closed. + * + * Otherwise the old session will always unconfigure if there is no new session to + * replace it. + * + * Unconfiguring could add hundreds of milliseconds of delay. We could race and attempt + * to skip unconfigure if a new session is created before the captures are all drained, + * but this would introduce nondeterministic behavior. + */ + + // #close was already called explicitly, keep going the slow route + if (mClosed) { + return; + } + + mSkipUnconfigure = true; + close(); + } + + @Override + public synchronized void close() { + if (mClosed) { + return; + } + + mClosed = true; + + /* + * Flush out any repeating request. Since camera is closed, no new requests + * can be queued, and eventually the entire request queue will be drained. + * + * Once this is done, wait for camera to idle, then unconfigure the camera. + * Once that's done, fire #onClosed. + */ + try { + mDeviceImpl.stopRepeating(); + } catch (CameraAccessException e) { + // OK: close does not throw checked exceptions. + Log.e(TAG, "Exception while stopping repeating: ", e); + + // TODO: call onError instead of onClosed if this happens + } + + // If no sequences are pending, fire #onClosed immediately + mSequenceDrainer.beginDrain(); + } + + /** + * Post calls into a CameraCaptureSession.StateListener to the user-specified {@code handler}. + */ + private StateListener createUserStateListenerProxy(Handler handler, StateListener listener) { + InvokeDispatcher<StateListener> userListenerSink = new InvokeDispatcher<>(listener); + HandlerDispatcher<StateListener> handlerPassthrough = + new HandlerDispatcher<>(userListenerSink, handler); + + return new ListenerProxies.SessionStateListenerProxy(handlerPassthrough); + } + + /** + * Forward callbacks from + * CameraDevice.CaptureListener to the CameraCaptureSession.CaptureListener. + * + * <p>In particular, all calls are automatically split to go both to our own + * internal listener, and to the user-specified listener (by transparently posting + * to the user-specified handler).</p> + * + * <p>When a capture sequence finishes, update the pending checked sequences set.</p> + */ + @SuppressWarnings("deprecation") + private CameraDevice.CaptureListener createCaptureListenerProxy( + Handler handler, CaptureListener listener) { + CameraDevice.CaptureListener localListener = new CameraDevice.CaptureListener() { + @Override + public void onCaptureSequenceCompleted(CameraDevice camera, + int sequenceId, long frameNumber) { + finishPendingSequence(sequenceId); + } + + @Override + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { + finishPendingSequence(sequenceId); + } + }; + + /* + * Split the calls from the device listener into local listener and the following chain: + * - replace the first CameraDevice arg with a CameraCaptureSession + * - duck type from device listener to session listener + * - then forward the call to a handler + * - then finally invoke the destination method on the session listener object + */ + Dispatchable<CaptureListener> userListenerSink; + if (listener == null) { // OK: API allows the user to not specify a listener + userListenerSink = new NullDispatcher<>(); + } else { + userListenerSink = new InvokeDispatcher<>(listener); + } + + InvokeDispatcher<CameraDevice.CaptureListener> localSink = + new InvokeDispatcher<>(localListener); + HandlerDispatcher<CaptureListener> handlerPassthrough = + new HandlerDispatcher<>(userListenerSink, handler); + DuckTypingDispatcher<CameraDevice.CaptureListener, CaptureListener> duckToSession + = new DuckTypingDispatcher<>(handlerPassthrough, CaptureListener.class); + ArgumentReplacingDispatcher<CameraDevice.CaptureListener, CameraCaptureSessionImpl> + replaceDeviceWithSession = new ArgumentReplacingDispatcher<>(duckToSession, + /*argumentIndex*/0, this); + + BroadcastDispatcher<CameraDevice.CaptureListener> broadcaster = + new BroadcastDispatcher<CameraDevice.CaptureListener>( + replaceDeviceWithSession, + localSink); + + return new ListenerProxies.DeviceCaptureListenerProxy(broadcaster); + } + + /** + * + * Create an internal state listener, to be invoked on the mDeviceHandler + * + * <p>It has a few behaviors: + * <ul> + * <li>Convert device state changes into session state changes. + * <li>Keep track of async tasks that the session began (idle, abort). + * </ul> + * </p> + * */ + CameraDevice.StateListener getDeviceStateListener() { + final CameraCaptureSession session = this; + + return new CameraDevice.StateListener() { + private boolean mBusy = false; + private boolean mActive = false; + + @Override + public void onOpened(CameraDevice camera) { + throw new AssertionError("Camera must already be open before creating a session"); + } + + @Override + public void onDisconnected(CameraDevice camera) { + close(); + } + + @Override + public void onError(CameraDevice camera, int error) { + // TODO: Handle errors somehow. + Log.wtf(TAG, "Got device error " + error); + } + + @Override + public void onActive(CameraDevice camera) { + mIdleDrainer.taskStarted(); + mActive = true; + + mStateListener.onActive(session); + } + + @Override + public void onIdle(CameraDevice camera) { + boolean isAborting; + synchronized (session) { + isAborting = mAborting; + } + + /* + * Check which states we transitioned through: + * + * (ACTIVE -> IDLE) + * (BUSY -> IDLE) + * + * Note that this is also legal: + * (ACTIVE -> BUSY -> IDLE) + * + * and mark those tasks as finished + */ + if (mBusy && isAborting) { + mAbortDrainer.taskFinished(); + + synchronized (session) { + mAborting = false; + } + } + + if (mActive) { + mIdleDrainer.taskFinished(); + } + + mBusy = false; + mActive = false; + + mStateListener.onReady(session); + } + + @Override + public void onBusy(CameraDevice camera) { + mBusy = true; + + // TODO: Queue captures during abort instead of failing them + // since the app won't be able to distinguish the two actives + Log.w(TAG, "Device is now busy; do not submit new captures (TODO: allow this)"); + mStateListener.onActive(session); + } + + @Override + public void onUnconfigured(CameraDevice camera) { + synchronized (session) { + // Ignore #onUnconfigured before #close is called + if (mClosed) { + mUnconfigureDrainer.taskFinished(); + } + } + } + }; + + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } + + private void checkLegalToCapture() { + if (mAborting) { + throw new IllegalStateException( + "Session is aborting captures; new captures are not permitted"); + } + } + + private void checkNotClosed() { + if (mClosed) { + throw new IllegalStateException( + "Session has been closed; further changes are illegal."); + } + } + + /** + * Notify the session that a pending capture sequence has just been queued. + * + * <p>During a shutdown/close, the session waits until all pending sessions are finished + * before taking any further steps to shut down itself.</p> + * + * @see #finishPendingSequence + */ + private int addPendingSequence(int sequenceId) { + mSequenceDrainer.taskStarted(sequenceId); + return sequenceId; + } + + /** + * Notify the session that a pending capture sequence is now finished. + * + * <p>During a shutdown/close, once all pending sequences finish, it is safe to + * close the camera further by unconfiguring and then firing {@code onClosed}.</p> + */ + private void finishPendingSequence(int sequenceId) { + mSequenceDrainer.taskFinished(sequenceId); + } + + private class SequenceDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + /* + * No repeating request is set; and the capture queue has fully drained. + * + * If no captures were queued to begin with, and an abort was queued, + * it's still possible to get another BUSY before the last IDLE. + * + * If the camera is already "IDLE" and no aborts are pending, + * then the drain immediately finishes. + */ + mAbortDrainer.beginDrain(); + } + } + + private class AbortDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + /* + * Any queued aborts have now completed. + * + * It's now safe to wait to receive the final "IDLE" event, as the camera device + * will no longer again transition to "ACTIVE" by itself. + * + * If the camera is already "IDLE", then the drain immediately finishes. + */ + mIdleDrainer.beginDrain(); + } + } + } + + private class IdleDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + /* + * The device is now IDLE, and has settled. It will not transition to + * ACTIVE or BUSY again by itself. + * + * It's now safe to unconfigure the outputs and after it's done invoke #onClosed. + * + * This operation is idempotent; a session will not be closed twice. + */ + + // Fast path: A new capture session has replaced this one; don't unconfigure. + if (mSkipUnconfigure) { + mStateListener.onClosed(CameraCaptureSessionImpl.this); + return; + } + + // Slow path: #close was called explicitly on this session; unconfigure first + + try { + mUnconfigureDrainer.taskStarted(); + mDeviceImpl.configureOutputs(null); // begin transition to unconfigured state + } catch (CameraAccessException e) { + // OK: do not throw checked exceptions. + Log.e(TAG, "Exception while configuring outputs: ", e); + + // TODO: call onError instead of onClosed if this happens + } + + mUnconfigureDrainer.beginDrain(); + } + } + } + + private class UnconfigureDrainListener implements TaskDrainer.DrainListener { + @Override + public void onDrained() { + synchronized (CameraCaptureSessionImpl.this) { + // The device has finished unconfiguring. It's now fully closed. + mStateListener.onClosed(CameraCaptureSessionImpl.this); + } + } + } +} diff --git a/core/java/android/hardware/camera2/impl/CameraDevice.java b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java index 988f8f9..d4adae1 100644 --- a/core/java/android/hardware/camera2/impl/CameraDevice.java +++ b/core/java/android/hardware/camera2/impl/CameraDeviceImpl.java @@ -19,14 +19,16 @@ 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.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; @@ -45,7 +47,7 @@ import java.util.TreeSet; /** * HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate */ -public class CameraDevice implements android.hardware.camera2.CameraDevice { +public class CameraDeviceImpl extends android.hardware.camera2.CameraDevice { private final String TAG; private final boolean DEBUG; @@ -59,10 +61,13 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final CameraDeviceCallbacks mCallbacks = new CameraDeviceCallbacks(); private final StateListener mDeviceListener; + private volatile StateListener mSessionStateListener; private final Handler mDeviceHandler; + private boolean mInError = false; private boolean mIdle = true; + /** map request IDs to listener/request data */ private final SparseArray<CaptureListenerHolder> mCaptureListenerMap = new SparseArray<CaptureListenerHolder>(); @@ -72,6 +77,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. @@ -86,14 +92,20 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { */ private final FrameNumberTracker mFrameNumberTracker = new FrameNumberTracker(); + private CameraCaptureSessionImpl mCurrentSession; + // Runnables for all state transitions, except error, which needs the // error code argument private final Runnable mCallOnOpened = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onOpened(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onOpened(CameraDeviceImpl.this); + } + mDeviceListener.onOpened(CameraDeviceImpl.this); } } }; @@ -101,8 +113,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final Runnable mCallOnUnconfigured = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onUnconfigured(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onUnconfigured(CameraDeviceImpl.this); + } + mDeviceListener.onUnconfigured(CameraDeviceImpl.this); } } }; @@ -110,8 +126,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final Runnable mCallOnActive = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onActive(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onActive(CameraDeviceImpl.this); + } + mDeviceListener.onActive(CameraDeviceImpl.this); } } }; @@ -119,8 +139,12 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final Runnable mCallOnBusy = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onBusy(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onBusy(CameraDeviceImpl.this); + } + mDeviceListener.onBusy(CameraDeviceImpl.this); } } }; @@ -128,15 +152,23 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final Runnable mCallOnClosed = new Runnable() { @Override public void run() { - mDeviceListener.onClosed(CameraDevice.this); + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onClosed(CameraDeviceImpl.this); + } + mDeviceListener.onClosed(CameraDeviceImpl.this); } }; private final Runnable mCallOnIdle = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onIdle(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onIdle(CameraDeviceImpl.this); + } + mDeviceListener.onIdle(CameraDeviceImpl.this); } } }; @@ -144,19 +176,25 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private final Runnable mCallOnDisconnected = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onDisconnected(CameraDevice.this); + if (!CameraDeviceImpl.this.isClosed()) { + StateListener sessionListener = mSessionStateListener; + if (sessionListener != null) { + sessionListener.onDisconnected(CameraDeviceImpl.this); + } + mDeviceListener.onDisconnected(CameraDeviceImpl.this); } } }; - public CameraDevice(String cameraId, StateListener listener, Handler handler) { + public CameraDeviceImpl(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); @@ -164,7 +202,6 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { tag = tag.substring(0, MAX_TAG_LEN); } TAG = tag; - DEBUG = Log.isLoggable(TAG, Log.DEBUG); } @@ -175,6 +212,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void setRemoteDevice(ICameraDeviceUser remoteDevice) { // TODO: Move from decorator to direct binder-mediated exceptions synchronized(mLock) { + // If setRemoteFailure already called, do nothing + if (mInError) return; + mRemoteDevice = CameraBinderDecorator.newInstance(remoteDevice); mDeviceHandler.post(mCallOnOpened); @@ -182,6 +222,52 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } } + /** + * Call to indicate failed connection to a remote camera device. + * + * <p>This places the camera device in the error state and informs the listener. + * Use in place of setRemoteDevice() when startup fails.</p> + */ + public void setRemoteFailure(final CameraRuntimeException failure) { + int failureCode = StateListener.ERROR_CAMERA_DEVICE; + boolean failureIsError = true; + + switch (failure.getReason()) { + case CameraAccessException.CAMERA_IN_USE: + failureCode = StateListener.ERROR_CAMERA_IN_USE; + break; + case CameraAccessException.MAX_CAMERAS_IN_USE: + failureCode = StateListener.ERROR_MAX_CAMERAS_IN_USE; + break; + case CameraAccessException.CAMERA_DISABLED: + failureCode = StateListener.ERROR_CAMERA_DISABLED; + break; + case CameraAccessException.CAMERA_DISCONNECTED: + failureIsError = false; + break; + case CameraAccessException.CAMERA_ERROR: + failureCode = StateListener.ERROR_CAMERA_DEVICE; + break; + default: + Log.wtf(TAG, "Unknown failure in opening camera device: " + failure.getReason()); + break; + } + final int code = failureCode; + final boolean isError = failureIsError; + synchronized (mLock) { + mInError = true; + mDeviceHandler.post(new Runnable() { + public void run() { + if (isError) { + mDeviceListener.onError(CameraDeviceImpl.this, code); + } else { + mDeviceListener.onDisconnected(CameraDeviceImpl.this); + } + } + }); + } + } + @Override public String getId() { return mCameraId; @@ -194,7 +280,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { outputs = new ArrayList<Surface>(); } synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); HashSet<Surface> addSet = new HashSet<Surface>(outputs); // Streams to create List<Integer> deleteList = new ArrayList<Integer>(); // Streams to delete @@ -217,7 +303,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 +318,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,15 +340,58 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } @Override + public void createCaptureSession(List<Surface> outputs, + CameraCaptureSession.StateListener listener, Handler handler) + throws CameraAccessException { + synchronized (mLock) { + if (DEBUG) { + Log.d(TAG, "createCaptureSession"); + } + + checkIfCameraClosedOrInError(); + + // TODO: we must be in UNCONFIGURED mode to begin with, or using another session + + // TODO: dont block for this + boolean configureSuccess = true; + CameraAccessException pendingException = null; + try { + configureOutputs(outputs); // and then block until IDLE + } catch (CameraAccessException e) { + configureSuccess = false; + pendingException = e; + } + + // Fire onConfigured if configureOutputs succeeded, fire onConfigureFailed otherwise. + CameraCaptureSessionImpl newSession = + new CameraCaptureSessionImpl(outputs, listener, handler, this, mDeviceHandler, + configureSuccess); + + if (mCurrentSession != null) { + mCurrentSession.replaceSessionClose(newSession); + } + + // TODO: wait until current session closes, then create the new session + mCurrentSession = newSession; + + if (pendingException != null) { + throw pendingException; + } + + mSessionStateListener = mCurrentSession.getDeviceStateListener(); + } + } + + @Override public CaptureRequest.Builder createCaptureRequest(int templateType) throws CameraAccessException { synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); CameraMetadataNative templatedRequest = new CameraMetadataNative(); try { - mRemoteDevice.createDefaultRequest(templateType, /* out */templatedRequest); + mRemoteDevice.createDefaultRequest(templateType, /*out*/templatedRequest); } catch (CameraRuntimeException e) { throw e.asChecked(); } catch (RemoteException e) { @@ -291,10 +420,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int captureBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { - // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. - if (requests.isEmpty()) { - Log.w(TAG, "Capture burst request list is empty, do nothing!"); - return -1; + if (requests == null || requests.isEmpty()) { + throw new IllegalArgumentException("At least one request must be given"); } return submitCaptureRequest(requests, listener, handler, /*streaming*/false); } @@ -339,7 +466,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { Runnable resultDispatch = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { + if (!CameraDeviceImpl.this.isClosed()) { if (DEBUG) { Log.d(TAG, String.format( "early trigger sequence complete for request %d", @@ -350,9 +477,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { throw new AssertionError(lastFrameNumber + " cannot be cast to int"); } holder.getListener().onCaptureSequenceCompleted( - CameraDevice.this, + CameraDeviceImpl.this, requestId, - (int)lastFrameNumber); + lastFrameNumber); } } }; @@ -379,7 +506,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); int requestId; if (repeating) { @@ -441,10 +568,8 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public int setRepeatingBurst(List<CaptureRequest> requests, CaptureListener listener, Handler handler) throws CameraAccessException { - // TODO: remove this. Throw IAE if the request is null or empty. Need to update API doc. - if (requests.isEmpty()) { - Log.w(TAG, "Set Repeating burst request list is empty, do nothing!"); - return -1; + if (requests == null || requests.isEmpty()) { + throw new IllegalArgumentException("At least one request must be given"); } return submitCaptureRequest(requests, listener, handler, /*streaming*/true); } @@ -453,7 +578,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { public void stopRepeating() throws CameraAccessException { synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); if (mRepeatingRequestId != REQUEST_ID_NONE) { int requestId = mRepeatingRequestId; @@ -484,7 +609,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { private void waitUntilIdle() throws CameraAccessException { synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); if (mRepeatingRequestId != REQUEST_ID_NONE) { throw new IllegalStateException("Active repeating request ongoing"); } @@ -505,7 +630,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { @Override public void flush() throws CameraAccessException { synchronized (mLock) { - checkIfCameraClosed(); + checkIfCameraClosedOrInError(); mDeviceHandler.post(mCallOnBusy); try { @@ -539,11 +664,15 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { // impossible } - if (mRemoteDevice != null) { + // Only want to fire the onClosed callback once; + // either a normal close where the remote device is valid + // or a close after a startup error (no remote device but in error state) + if (mRemoteDevice != null || mInError) { mDeviceHandler.post(mCallOnClosed); } mRemoteDevice = null; + mInError = false; } } @@ -694,7 +823,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { Runnable resultDispatch = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()){ + if (!CameraDeviceImpl.this.isClosed()){ if (DEBUG) { Log.d(TAG, String.format( "fire sequence complete for request %d", @@ -708,9 +837,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { + " cannot be cast to int"); } holder.getListener().onCaptureSequenceCompleted( - CameraDevice.this, + CameraDeviceImpl.this, requestId, - (int)lastFrameNumber); + lastFrameNumber); } } }; @@ -760,6 +889,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { if (isClosed()) return; synchronized(mLock) { + mInError = true; switch (errorCode) { case ERROR_CAMERA_DISCONNECTED: r = mCallOnDisconnected; @@ -772,14 +902,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { r = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { - mDeviceListener.onError(CameraDevice.this, errorCode); + if (!CameraDeviceImpl.this.isClosed()) { + mDeviceListener.onError(CameraDeviceImpl.this, errorCode); } } }; break; } - CameraDevice.this.mDeviceHandler.post(r); + CameraDeviceImpl.this.mDeviceHandler.post(r); } // Fire onCaptureSequenceCompleted @@ -799,10 +929,10 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { Log.d(TAG, "Camera now idle"); } synchronized (mLock) { - if (!CameraDevice.this.mIdle) { - CameraDevice.this.mDeviceHandler.post(mCallOnIdle); + if (!CameraDeviceImpl.this.mIdle) { + CameraDeviceImpl.this.mDeviceHandler.post(mCallOnIdle); } - CameraDevice.this.mIdle = true; + CameraDeviceImpl.this.mIdle = true; } } @@ -816,7 +946,7 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { // Get the listener for this frame ID, if there is one synchronized (mLock) { - holder = CameraDevice.this.mCaptureListenerMap.get(requestId); + holder = CameraDeviceImpl.this.mCaptureListenerMap.get(requestId); } if (holder == null) { @@ -830,9 +960,9 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()) { + if (!CameraDeviceImpl.this.isClosed()) { holder.getListener().onCaptureStarted( - CameraDevice.this, + CameraDeviceImpl.this, holder.getRequest(resultExtras.getSubsequenceId()), timestamp); } @@ -843,14 +973,21 @@ 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); + holder = CameraDeviceImpl.this.mCaptureListenerMap.get(requestId); } Boolean quirkPartial = result.get(CaptureResult.QUIRKS_PARTIAL_RESULT); @@ -881,32 +1018,38 @@ 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 public void run() { - if (!CameraDevice.this.isClosed()){ + if (!CameraDeviceImpl.this.isClosed()){ holder.getListener().onCapturePartial( - CameraDevice.this, + CameraDeviceImpl.this, request, resultAsCapture); } } }; } else { + final TotalCaptureResult resultAsCapture = + new TotalCaptureResult(result, request, requestId); + // Final capture result resultDispatch = new Runnable() { @Override public void run() { - if (!CameraDevice.this.isClosed()){ + if (!CameraDeviceImpl.this.isClosed()){ holder.getListener().onCaptureCompleted( - CameraDevice.this, + CameraDeviceImpl.this, request, resultAsCapture); } @@ -925,10 +1068,14 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { } /** - * Default handler management. If handler is null, get the current thread's - * Looper to create a Handler with. If no looper exists, throw exception. + * Default handler management. + * + * <p> + * If handler is null, get the current thread's + * Looper to create a Handler with. If no looper exists, throw {@code IllegalArgumentException}. + * </p> */ - private Handler checkHandler(Handler handler) { + static Handler checkHandler(Handler handler) { if (handler == null) { Looper looper = Looper.myLooper(); if (looper == null) { @@ -940,7 +1087,11 @@ public class CameraDevice implements android.hardware.camera2.CameraDevice { return handler; } - private void checkIfCameraClosed() { + private void checkIfCameraClosedOrInError() throws CameraAccessException { + if (mInError) { + throw new CameraAccessException(CameraAccessException.CAMERA_ERROR, + "The camera device has encountered a serious error"); + } if (mRemoteDevice == null) { throw new IllegalStateException("CameraDevice was already closed"); } @@ -951,4 +1102,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 9a06e97..83aee5d 100644 --- a/core/java/android/hardware/camera2/impl/CameraMetadataNative.java +++ b/core/java/android/hardware/camera2/impl/CameraMetadataNative.java @@ -20,9 +20,8 @@ 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.marshal.Marshaler; import android.hardware.camera2.marshal.MarshalQueryable; import android.hardware.camera2.marshal.MarshalRegistry; @@ -32,6 +31,7 @@ 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; @@ -43,24 +43,210 @@ 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 com.android.internal.util.Preconditions; + +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; +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(); @@ -81,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 @@ -106,11 +306,39 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { nativeWriteToParcel(dest); } - @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); @@ -149,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 @@ -185,6 +425,18 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { mMetadataPtr = 0; // set it to 0 again to prevent eclipse from making this field final } + private <T> T getBase(CameraCharacteristics.Key<T> key) { + return getBase(key.getNativeKey()); + } + + private <T> T getBase(CaptureResult.Key<T> key) { + return getBase(key.getNativeKey()); + } + + private <T> T getBase(CaptureRequest.Key<T> key) { + return getBase(key.getNativeKey()); + } + private <T> T getBase(Key<T> key) { int tag = key.getTag(); byte[] values = readValues(tag); @@ -196,25 +448,44 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { ByteBuffer buffer = ByteBuffer.wrap(values).order(ByteOrder.nativeOrder()); 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() { @@ -231,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; @@ -374,6 +601,142 @@ 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(); @@ -401,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) { @@ -500,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(); @@ -516,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; @@ -607,6 +948,22 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { return nativeReadValues(tag); } + /** + * 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() { + try { + nativeDump(); + } catch (IOException e) { + Log.wtf(TAG, "Dump logging failed", e); + } + } + @Override protected void finalize() throws Throwable { try { @@ -650,6 +1007,7 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { new MarshalQueryableString(), new MarshalQueryableReprocessFormatsMap(), new MarshalQueryableRange(), + new MarshalQueryablePair(), new MarshalQueryableMeteringRectangle(), new MarshalQueryableColorSpaceTransform(), new MarshalQueryableStreamConfiguration(), @@ -675,5 +1033,4 @@ public class CameraMetadataNative extends CameraMetadata implements Parcelable { 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/ListenerProxies.java b/core/java/android/hardware/camera2/impl/ListenerProxies.java new file mode 100644 index 0000000..04c43e3 --- /dev/null +++ b/core/java/android/hardware/camera2/impl/ListenerProxies.java @@ -0,0 +1,168 @@ +package android.hardware.camera2.impl; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.dispatch.Dispatchable; +import android.hardware.camera2.dispatch.MethodNameInvoker; + +import static com.android.internal.util.Preconditions.*; + +/** + * Proxy out invocations to the camera2 API listeners into a {@link Dispatchable}. + * + * <p>Since abstract classes do not support Java's dynamic {@code Proxy}, we have to + * to use our own proxy mechanism.</p> + */ +public class ListenerProxies { + + // TODO: replace with codegen + + public static class DeviceStateListenerProxy extends CameraDevice.StateListener { + private final MethodNameInvoker<CameraDevice.StateListener> mProxy; + + public DeviceStateListenerProxy( + Dispatchable<CameraDevice.StateListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.StateListener.class); + } + + @Override + public void onOpened(CameraDevice camera) { + mProxy.invoke("onOpened", camera); + } + + @Override + public void onDisconnected(CameraDevice camera) { + mProxy.invoke("onDisconnected", camera); + } + + @Override + public void onError(CameraDevice camera, int error) { + mProxy.invoke("onError", camera, error); + } + + @Override + public void onUnconfigured(CameraDevice camera) { + mProxy.invoke("onUnconfigured", camera); + } + + @Override + public void onActive(CameraDevice camera) { + mProxy.invoke("onActive", camera); + } + + @Override + public void onBusy(CameraDevice camera) { + mProxy.invoke("onBusy", camera); + } + + @Override + public void onClosed(CameraDevice camera) { + mProxy.invoke("onClosed", camera); + } + + @Override + public void onIdle(CameraDevice camera) { + mProxy.invoke("onIdle", camera); + } + } + + @SuppressWarnings("deprecation") + public static class DeviceCaptureListenerProxy extends CameraDevice.CaptureListener { + private final MethodNameInvoker<CameraDevice.CaptureListener> mProxy; + + public DeviceCaptureListenerProxy( + Dispatchable<CameraDevice.CaptureListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, CameraDevice.CaptureListener.class); + } + + @Override + public void onCaptureStarted(CameraDevice camera, + CaptureRequest request, long timestamp) { + mProxy.invoke("onCaptureStarted", camera, request, timestamp); + } + + @Override + public void onCapturePartial(CameraDevice camera, + CaptureRequest request, CaptureResult result) { + mProxy.invoke("onCapturePartial", camera, request, result); + } + + @Override + public void onCaptureProgressed(CameraDevice camera, + CaptureRequest request, CaptureResult partialResult) { + mProxy.invoke("onCaptureProgressed", camera, request, partialResult); + } + + @Override + public void onCaptureCompleted(CameraDevice camera, + CaptureRequest request, TotalCaptureResult result) { + mProxy.invoke("onCaptureCompleted", camera, request, result); + } + + @Override + public void onCaptureFailed(CameraDevice camera, + CaptureRequest request, CaptureFailure failure) { + mProxy.invoke("onCaptureFailed", camera, request, failure); + } + + @Override + public void onCaptureSequenceCompleted(CameraDevice camera, + int sequenceId, long frameNumber) { + mProxy.invoke("onCaptureSequenceCompleted", camera, sequenceId, frameNumber); + } + + @Override + public void onCaptureSequenceAborted(CameraDevice camera, + int sequenceId) { + mProxy.invoke("onCaptureSequenceAborted", camera, sequenceId); + } + } + + public static class SessionStateListenerProxy + extends CameraCaptureSession.StateListener { + private final MethodNameInvoker<CameraCaptureSession.StateListener> mProxy; + + public SessionStateListenerProxy( + Dispatchable<CameraCaptureSession.StateListener> dispatchTarget) { + dispatchTarget = checkNotNull(dispatchTarget, "dispatchTarget must not be null"); + mProxy = new MethodNameInvoker<>(dispatchTarget, + CameraCaptureSession.StateListener.class); + } + + @Override + public void onConfigured(CameraCaptureSession session) { + mProxy.invoke("onConfigured", session); + } + + + @Override + public void onConfigureFailed(CameraCaptureSession session) { + mProxy.invoke("onConfigureFailed", session); + } + + @Override + public void onReady(CameraCaptureSession session) { + mProxy.invoke("onReady", session); + } + + @Override + public void onActive(CameraCaptureSession session) { + mProxy.invoke("onActive", session); + } + + @Override + public void onClosed(CameraCaptureSession session) { + mProxy.invoke("onClosed", session); + } + } + + private ListenerProxies() { + throw new AssertionError(); + } +} 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 index fd72ee2..35ecc2a 100644 --- a/core/java/android/hardware/camera2/marshal/MarshalHelpers.java +++ b/core/java/android/hardware/camera2/marshal/MarshalHelpers.java @@ -18,8 +18,8 @@ package android.hardware.camera2.marshal; import static android.hardware.camera2.impl.CameraMetadataNative.*; import static com.android.internal.util.Preconditions.*; -import android.hardware.camera2.Rational; import android.hardware.camera2.impl.CameraMetadataNative; +import android.util.Rational; /** * Static functions in order to help implementing various marshaler functionality. diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java index d3796db..47f79bf 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableColorSpaceTransform.java @@ -15,9 +15,9 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.ColorSpaceTransform; 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; diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java index c8b9bd8..01780db 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableMeteringRectangle.java @@ -15,9 +15,9 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.MeteringRectangle; 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; 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/MarshalQueryablePrimitive.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java index 708da70..189b597 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryablePrimitive.java @@ -15,11 +15,11 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.Rational; 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.*; diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java index 3025cb4..98a7ad7 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableReprocessFormatsMap.java @@ -15,9 +15,10 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.ReprocessFormatsMap; 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.*; @@ -50,12 +51,13 @@ public class MarshalQueryableReprocessFormatsMap * INPUT_FORMAT, OUTPUT_FORMAT_COUNT, [OUTPUT_0, OUTPUT_1, ..., OUTPUT_FORMAT_COUNT-1] * }; */ - int[] inputs = value.getInputs(); + int[] inputs = StreamConfigurationMap.imageFormatToInternal(value.getInputs()); for (int input : inputs) { // INPUT_FORMAT buffer.putInt(input); - int[] outputs = value.getOutputs(input); + int[] outputs = + StreamConfigurationMap.imageFormatToInternal(value.getOutputs(input)); // OUTPUT_FORMAT_COUNT buffer.putInt(outputs.length); diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java index 93c0e92..4253a0a 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableRggbChannelVector.java @@ -15,9 +15,9 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.RggbChannelVector; 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; diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java index 6a73bee..721644e 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableSize.java @@ -15,7 +15,7 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.Size; +import android.util.Size; import android.hardware.camera2.marshal.Marshaler; import android.hardware.camera2.marshal.MarshalQueryable; import android.hardware.camera2.utils.TypeReference; diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java index 6a4e821..62ace31 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfiguration.java @@ -15,9 +15,9 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.StreamConfiguration; 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.*; diff --git a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java index c3d564e..fd3dfac 100644 --- a/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java +++ b/core/java/android/hardware/camera2/marshal/impl/MarshalQueryableStreamConfigurationDuration.java @@ -15,9 +15,9 @@ */ package android.hardware.camera2.marshal.impl; -import android.hardware.camera2.StreamConfigurationDuration; 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.*; diff --git a/core/java/android/hardware/camera2/ColorSpaceTransform.java b/core/java/android/hardware/camera2/params/ColorSpaceTransform.java index 5e4c0a2..b4289db 100644 --- a/core/java/android/hardware/camera2/ColorSpaceTransform.java +++ b/core/java/android/hardware/camera2/params/ColorSpaceTransform.java @@ -14,11 +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.CameraMetadata; import android.hardware.camera2.utils.HashCodeHelpers; +import android.util.Rational; import java.util.Arrays; @@ -137,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); } @@ -160,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"); } @@ -195,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 2b0108c..9bbc33a 100644 --- a/core/java/android/hardware/camera2/LensShadingMap.java +++ b/core/java/android/hardware/camera2/params/LensShadingMap.java @@ -14,11 +14,13 @@ * 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.CameraCharacteristics; +import android.hardware.camera2.CaptureResult; import android.hardware.camera2.utils.HashCodeHelpers; import java.util.Arrays; @@ -26,7 +28,7 @@ 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 bb8e5b1..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.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) { @@ -208,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 894a499..d3f5bc3 100644 --- a/core/java/android/hardware/camera2/ReprocessFormatsMap.java +++ b/core/java/android/hardware/camera2/params/ReprocessFormatsMap.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package android.hardware.camera2; +package android.hardware.camera2.params; import static com.android.internal.util.Preconditions.*; +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 a514034..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.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; import android.hardware.camera2.utils.HashCodeHelpers; +import android.graphics.PixelFormat; import android.util.Size; /** @@ -62,11 +65,12 @@ public final class StreamConfiguration { } /** - * 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 6a31156..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.CameraCharacteristics; import android.hardware.camera2.utils.HashCodeHelpers; +import android.graphics.PixelFormat; import android.util.Size; /** @@ -59,11 +61,12 @@ public final class StreamConfigurationDuration { } /** - * 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 2958ebf..481d67a 100644 --- a/core/java/android/hardware/camera2/TonemapCurve.java +++ b/core/java/android/hardware/camera2/params/TonemapCurve.java @@ -14,11 +14,15 @@ * 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.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; @@ -74,7 +78,7 @@ public final class TonemapCurve { /** * Create a new immutable TonemapCurve instance. * - * <p>Values are stored as a contiguous {@code (Pin, Pout}) point.</p> + * <p>Values are stored as a contiguous array of {@code (Pin, Pout)} points.</p> * * <p>All parameters may have independent length but should have at most * {@link CameraCharacteristics#TONEMAP_MAX_CURVE_POINTS} * {@value #POINT_SIZE} elements.</p> @@ -84,15 +88,16 @@ public final class TonemapCurve { * * <p>This constructor copies the array contents and does not retain ownership of the array.</p> * - * @param elements An array of elements whose length is {@code CHANNEL_COUNT * rows * columns} + * @param red An array of elements whose length is divisible by {@value #POINT_SIZE} + * @param green An array of elements whose length is divisible by {@value #POINT_SIZE} + * @param blue An array of elements whose length is divisible by {@value #POINT_SIZE} * * @throws IllegalArgumentException - * if the {@code elements} array length is invalid, - * if any of the subelems are not finite + * if any of input array length is invalid, + * or if any of the elements in the array are not in the range of + * [{@value #LEVEL_BLACK}, {@value #LEVEL_WHITE}] * @throws NullPointerException - * if any of the parameters is {@code null} - * - * @hide + * if any of the parameters are {@code null} */ public TonemapCurve(float[] red, float[] green, float[] blue) { // TODO: maxCurvePoints check? 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/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/TaskDrainer.java b/core/java/android/hardware/camera2/utils/TaskDrainer.java new file mode 100644 index 0000000..3cba9a1 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/TaskDrainer.java @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 android.os.Handler; +import android.util.Log; + +import java.util.HashSet; +import java.util.Set; + +import static com.android.internal.util.Preconditions.*; + +/** + * Keep track of multiple concurrent tasks starting and finishing by their key; + * allow draining existing tasks and figuring out when all tasks have finished + * (and new ones won't begin). + * + * <p>The initial state is to allow all tasks to be started and finished. A task may only be started + * once, after which it must be finished before starting again. Likewise, finishing a task + * that hasn't been started is also not allowed.</p> + * + * <p>When draining begins, no more new tasks can be started. This guarantees that at some + * point when all the tasks are finished there will be no more collective new tasks, + * at which point the {@link DrainListener#onDrained} callback will be invoked.</p> + * + * + * @param <T> + * a type for the key that will represent tracked tasks; + * must implement {@code Object#equals} + */ +public class TaskDrainer<T> { + /** + * Fired asynchronously after draining has begun with {@link TaskDrainer#beginDrain} + * <em>and</em> all tasks that were started have finished. + */ + public interface DrainListener { + /** All tasks have fully finished draining; there will be no more pending tasks. */ + public void onDrained(); + } + + private static final String TAG = "TaskDrainer"; + private static final boolean VERBOSE = false; + + private final Handler mHandler; + private final DrainListener mListener; + private final String mName; + + /** Set of tasks which have been started but not yet finished with #taskFinished */ + private final Set<T> mTaskSet = new HashSet<T>(); + private final Object mLock = new Object(); + + private boolean mDraining = false; + private boolean mDrainFinished = false; + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + */ + public TaskDrainer(Handler handler, DrainListener listener) { + mHandler = checkNotNull(handler, "handler must not be null"); + mListener = checkNotNull(listener, "listener must not be null"); + mName = null; + } + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + * @param name an optional name used for debug logging + */ + public TaskDrainer(Handler handler, DrainListener listener, String name) { + // XX: Probably don't need a handler at all here + mHandler = checkNotNull(handler, "handler must not be null"); + mListener = checkNotNull(listener, "listener must not be null"); + mName = name; + } + + /** + * Mark an asynchronous task as having started. + * + * <p>A task cannot be started more than once without first having finished. Once + * draining begins with {@link #beginDrain}, no new tasks can be started.</p> + * + * @param task a key to identify a task + * + * @see #taskFinished + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already started (and not finished), + * or if attempting to start a task after draining has begun. + */ + public void taskStarted(T task) { + synchronized (mLock) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "taskStarted " + task); + } + + if (mDraining) { + throw new IllegalStateException("Can't start more tasks after draining has begun"); + } + + if (!mTaskSet.add(task)) { + throw new IllegalStateException("Task " + task + " was already started"); + } + } + } + + + /** + * Mark an asynchronous task as having finished. + * + * <p>A task cannot be finished if it hasn't started. Once finished, a task + * cannot be finished again (unless it's started again).</p> + * + * @param task a key to identify a task + * + * @see #taskStarted + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already finished (and not re-started), + */ + public void taskFinished(T task) { + synchronized (mLock) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "taskFinished " + task); + } + + if (!mTaskSet.remove(task)) { + throw new IllegalStateException("Task " + task + " was already finished"); + } + + // If this is the last finished task and draining has already begun, fire #onDrained + checkIfDrainFinished(); + } + } + + /** + * Do not allow any more tasks to be started; once all existing started tasks are finished, + * fire the {@link DrainListener#onDrained} callback asynchronously. + * + * <p>This operation is idempotent; calling it more than once has no effect.</p> + */ + public void beginDrain() { + synchronized (mLock) { + if (!mDraining) { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "beginDrain started"); + } + + mDraining = true; + + // If all tasks that had started had already finished by now, fire #onDrained + checkIfDrainFinished(); + } else { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "beginDrain ignored"); + } + } + } + } + + private void checkIfDrainFinished() { + if (mTaskSet.isEmpty() && mDraining && !mDrainFinished) { + mDrainFinished = true; + postDrained(); + } + } + + private void postDrained() { + mHandler.post(new Runnable() { + @Override + public void run() { + if (VERBOSE) { + Log.v(TAG + "[" + mName + "]", "onDrained"); + } + + mListener.onDrained(); + } + }); + } +} diff --git a/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java new file mode 100644 index 0000000..f6272c9 --- /dev/null +++ b/core/java/android/hardware/camera2/utils/TaskSingleDrainer.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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 android.hardware.camera2.utils.TaskDrainer.DrainListener; +import android.os.Handler; + +/** + * Keep track of a single concurrent task starting and finishing; + * allow draining the existing task and figuring out when the task has finished + * (and won't restart). + * + * <p>The initial state is to allow all tasks to be started and finished. A task may only be started + * once, after which it must be finished before starting again. Likewise, finishing a task + * that hasn't been started is also not allowed.</p> + * + * <p>When draining begins, the task cannot be started again. This guarantees that at some + * point the task will be finished forever, at which point the {@link DrainListener#onDrained} + * callback will be invoked.</p> + */ +public class TaskSingleDrainer { + + private final TaskDrainer<Object> mTaskDrainer; + private final Object mSingleTask = new Object(); + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + */ + public TaskSingleDrainer(Handler handler, DrainListener listener) { + mTaskDrainer = new TaskDrainer<>(handler, listener); + } + + /** + * Create a new task drainer; {@code onDrained} callbacks will be posted to the listener + * via the {@code handler}. + * + * @param handler a non-{@code null} handler to use to post runnables to + * @param listener a non-{@code null} listener where {@code onDrained} will be called + * @param name an optional name used for debug logging + */ + public TaskSingleDrainer(Handler handler, DrainListener listener, String name) { + mTaskDrainer = new TaskDrainer<>(handler, listener, name); + } + + /** + * Mark this asynchronous task as having started. + * + * <p>The task cannot be started more than once without first having finished. Once + * draining begins with {@link #beginDrain}, no new tasks can be started.</p> + * + * @see #taskFinished + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already started (and not finished), + * or if attempting to start a task after draining has begun. + */ + public void taskStarted() { + mTaskDrainer.taskStarted(mSingleTask); + } + + /** + * Do not allow any more task re-starts; once the existing task is finished, + * fire the {@link DrainListener#onDrained} callback asynchronously. + * + * <p>This operation is idempotent; calling it more than once has no effect.</p> + */ + public void beginDrain() { + mTaskDrainer.beginDrain(); + } + + /** + * Mark this asynchronous task as having finished. + * + * <p>The task cannot be finished if it hasn't started. Once finished, a task + * cannot be finished again (unless it's started again).</p> + * + * @see #taskStarted + * @see #beginDrain + * + * @throws IllegalStateException + * If attempting to start a task which is already finished (and not re-started), + */ + public void taskFinished() { + mTaskDrainer.taskFinished(mSingleTask); + } +} diff --git a/core/java/android/hardware/camera2/utils/UncheckedThrow.java b/core/java/android/hardware/camera2/utils/UncheckedThrow.java index 8224fed..ffcb78b 100644 --- a/core/java/android/hardware/camera2/utils/UncheckedThrow.java +++ b/core/java/android/hardware/camera2/utils/UncheckedThrow.java @@ -33,8 +33,20 @@ public class UncheckedThrow { UncheckedThrow.<RuntimeException>throwAnyImpl(e); } + /** + * Throw any kind of throwable without needing it to be checked + * @param e any instance of a Throwable + */ + public static void throwAnyException(Throwable e) { + /** + * Abuse type erasure by making the compiler think we are throwing RuntimeException, + * which is unchecked, but then inserting any exception in there. + */ + UncheckedThrow.<RuntimeException>throwAnyImpl(e); + } + @SuppressWarnings("unchecked") - private static<T extends Exception> void throwAnyImpl(Exception e) throws T { + private static<T extends Throwable> void throwAnyImpl(Throwable e) throws T { throw (T) e; } } 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 71fc780..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 String inputId, boolean isAvailable); +oneway interface IHdmiHotplugEventListener { + void onReceived(in HdmiHotplugEvent event); } diff --git a/core/java/android/hardware/soundtrigger/SoundTrigger.java b/core/java/android/hardware/soundtrigger/SoundTrigger.java new file mode 100644 index 0000000..2d7af85 --- /dev/null +++ b/core/java/android/hardware/soundtrigger/SoundTrigger.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.soundtrigger; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; + +import java.util.ArrayList; +import java.util.UUID; + +/** + * The SoundTrigger class provides access via JNI to the native service managing + * the sound trigger HAL. + * + * @hide + */ +public class SoundTrigger { + + public static final int STATUS_OK = 0; + public static final int STATUS_ERROR = Integer.MIN_VALUE; + public static final int STATUS_PERMISSION_DENIED = -1; + public static final int STATUS_NO_INIT = -19; + public static final int STATUS_BAD_VALUE = -22; + public static final int STATUS_DEAD_OBJECT = -32; + public static final int STATUS_INVALID_OPERATION = -38; + + /***************************************************************************** + * A ModuleProperties describes a given sound trigger hardware module + * managed by the native sound trigger service. Each module has a unique + * ID used to target any API call to this paricular module. Module + * properties are returned by listModules() method. + ****************************************************************************/ + public static class ModuleProperties { + /** Unique module ID provided by the native service */ + public final int id; + + /** human readable voice detection engine implementor */ + public final String implementor; + + /** human readable voice detection engine description */ + public final String description; + + /** Unique voice engine Id (changes with each version) */ + public final UUID uuid; + + /** Voice detection engine version */ + public final int version; + + /** Maximum number of active sound models */ + public final int maxSoundModels; + + /** Maximum number of key phrases */ + public final int maxKeyPhrases; + + /** Maximum number of users per key phrase */ + public final int maxUsers; + + /** Supported recognition modes (bit field, RECOGNITION_MODE_VOICE_TRIGGER ...) */ + public final int recognitionModes; + + /** Supports seamless transition to capture mode after recognition */ + public final boolean supportsCaptureTransition; + + /** Maximum buffering capacity in ms if supportsCaptureTransition() is true */ + public final int maxBufferMs; + + /** Supports capture by other use cases while detection is active */ + public final boolean supportsConcurrentCapture; + + /** Rated power consumption when detection is active with TDB silence/sound/speech ratio */ + public final int powerConsumptionMw; + + ModuleProperties(int id, String implementor, String description, + String uuid, int version, int maxSoundModels, int maxKeyPhrases, + int maxUsers, int recognitionModes, boolean supportsCaptureTransition, + int maxBufferMs, boolean supportsConcurrentCapture, + int powerConsumptionMw) { + this.id = id; + this.implementor = implementor; + this.description = description; + this.uuid = UUID.fromString(uuid); + this.version = version; + this.maxSoundModels = maxSoundModels; + this.maxKeyPhrases = maxKeyPhrases; + this.maxUsers = maxUsers; + this.recognitionModes = recognitionModes; + this.supportsCaptureTransition = supportsCaptureTransition; + this.maxBufferMs = maxBufferMs; + this.supportsConcurrentCapture = supportsConcurrentCapture; + this.powerConsumptionMw = powerConsumptionMw; + } + } + + /***************************************************************************** + * A SoundModel describes the attributes and contains the binary data used by the hardware + * implementation to detect a particular sound pattern. + * A specialized version {@link KeyPhraseSoundModel} is defined for key phrase + * sound models. + ****************************************************************************/ + public static class SoundModel { + /** Undefined sound model type */ + public static final int TYPE_UNKNOWN = -1; + + /** Keyphrase sound model */ + public static final int TYPE_KEYPHRASE = 0; + + /** Sound model type (e.g. TYPE_KEYPHRASE); */ + public final int type; + + /** Opaque data. For use by vendor implementation and enrollment application */ + public final byte[] data; + + public SoundModel(int type, byte[] data) { + this.type = type; + this.data = data; + } + } + + /***************************************************************************** + * A KeyPhrase describes a key phrase that can be detected by a + * {@link KeyPhraseSoundModel} + ****************************************************************************/ + public static class KeyPhrase { + /** Recognition modes supported for this key phrase in the model */ + public final int recognitionModes; + + /** Locale of the keyphrase. JAVA Locale string e.g en_US */ + public final String locale; + + /** Key phrase text */ + public final String text; + + /** Number of users this key phrase has been trained for */ + public final int numUsers; + + public KeyPhrase(int recognitionModes, String locale, String text, int numUsers) { + this.recognitionModes = recognitionModes; + this.locale = locale; + this.text = text; + this.numUsers = numUsers; + } + } + + /***************************************************************************** + * A KeyPhraseSoundModel is a specialized {@link SoundModel} for key phrases. + * It contains data needed by the hardware to detect a certain number of key phrases + * and the list of corresponding {@link KeyPhrase} descriptors. + ****************************************************************************/ + public static class KeyPhraseSoundModel extends SoundModel { + /** Key phrases in this sound model */ + public final KeyPhrase[] keyPhrases; // keyword phrases in model + + public KeyPhraseSoundModel(byte[] data, KeyPhrase[] keyPhrases) { + super(TYPE_KEYPHRASE, data); + this.keyPhrases = keyPhrases; + } + } + + /** + * Modes for key phrase recognition + */ + /** Simple recognition of the key phrase */ + public static final int RECOGNITION_MODE_VOICE_TRIGGER = 0x1; + /** Trigger only if one user is identified */ + public static final int RECOGNITION_MODE_USER_IDENTIFICATION = 0x2; + /** Trigger only if one user is authenticated */ + public static final int RECOGNITION_MODE_USER_AUTHENTICATION = 0x4; + + /** + * Status codes for {@link RecognitionEvent} + */ + /** Recognition success */ + public static final int RECOGNITION_STATUS_SUCCESS = 0; + /** Recognition aborted (e.g. capture preempted by anotehr use case */ + public static final int RECOGNITION_STATUS_ABORT = 1; + /** Recognition failure */ + public static final int RECOGNITION_STATUS_FAILURE = 2; + + /** + * A RecognitionEvent is provided by the + * {@link StatusListener#onRecognition(RecognitionEvent)} + * callback upon recognition success or failure. + */ + public static class RecognitionEvent { + /** Recognition status e.g {@link #RECOGNITION_STATUS_SUCCESS} */ + public final int status; + /** Sound Model corresponding to this event callback */ + public final int soundModelHandle; + /** True if it is possible to capture audio from this utterance buffered by the hardware */ + public final boolean captureAvailable; + /** Audio session ID to be used when capturing the utterance with an AudioRecord + * if captureAvailable() is true. */ + public final int captureSession; + /** Delay in ms between end of model detection and start of audio available for capture. + * A negative value is possible (e.g. if keyphrase is also available for capture) */ + public final int captureDelayMs; + /** Opaque data for use by system applications who know about voice engine internals, + * typically during enrollment. */ + public final byte[] data; + + RecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, + int captureSession, int captureDelayMs, byte[] data) { + this.status = status; + this.soundModelHandle = soundModelHandle; + this.captureAvailable = captureAvailable; + this.captureSession = captureSession; + this.captureDelayMs = captureDelayMs; + this.data = data; + } + } + + /** + * Additional data conveyed by a {@link KeyPhraseRecognitionEvent} + * for a key phrase detection. + */ + public static class KeyPhraseRecognitionExtra { + /** Confidence level for each user defined in the key phrase in the same order as + * users in the key phrase. The confidence level is expressed in percentage (0% -100%) */ + public final int[] confidenceLevels; + + /** Recognition modes matched for this event */ + public final int recognitionModes; + + KeyPhraseRecognitionExtra(int[] confidenceLevels, int recognitionModes) { + this.confidenceLevels = confidenceLevels; + this.recognitionModes = recognitionModes; + } + } + + /** + * Specialized {@link RecognitionEvent} for a key phrase detection. + */ + public static class KeyPhraseRecognitionEvent extends RecognitionEvent { + /** Indicates if the key phrase is present in the buffered audio available for capture */ + public final KeyPhraseRecognitionExtra[] keyPhraseExtras; + + /** Additional data available for each recognized key phrases in the model */ + public final boolean keyPhraseInCapture; + + KeyPhraseRecognitionEvent(int status, int soundModelHandle, boolean captureAvailable, + int captureSession, int captureDelayMs, byte[] data, + boolean keyPhraseInCapture, KeyPhraseRecognitionExtra[] keyPhraseExtras) { + super(status, soundModelHandle, captureAvailable, captureSession, captureDelayMs, data); + this.keyPhraseInCapture = keyPhraseInCapture; + this.keyPhraseExtras = keyPhraseExtras; + } + } + + /** + * Returns a list of descriptors for all harware modules loaded. + * @param modules A ModuleProperties array where the list will be returned. + * @return - {@link #STATUS_OK} in case of success + * - {@link #STATUS_ERROR} in case of unspecified error + * - {@link #STATUS_PERMISSION_DENIED} if the caller does not have system permission + * - {@link #STATUS_NO_INIT} if the native service cannot be reached + * - {@link #STATUS_BAD_VALUE} if modules is null + * - {@link #STATUS_DEAD_OBJECT} if the binder transaction to the native service fails + */ + public static native int listModules(ArrayList <ModuleProperties> modules); + + /** + * Get an interface on a hardware module to control sound models and recognition on + * this module. + * @param moduleId Sound module system identifier {@link ModuleProperties#id}. mandatory. + * @param listener {@link StatusListener} interface. Mandatory. + * @param handler the Handler that will receive the callabcks. Can be null if default handler + * is OK. + * @return a valid sound module in case of success or null in case of error. + */ + public static SoundTriggerModule attachModule(int moduleId, + StatusListener listener, + Handler handler) { + if (listener == null) { + return null; + } + SoundTriggerModule module = new SoundTriggerModule(moduleId, listener, handler); + return module; + } + + /** + * Interface provided by the client application when attaching to a {@link SoundTriggerModule} + * to received recognition and error notifications. + */ + public static interface StatusListener { + /** + * Called when recognition succeeds of fails + */ + public abstract void onRecognition(RecognitionEvent event); + + /** + * Called when the sound trigger native service dies + */ + public abstract void onServiceDied(); + } +} diff --git a/core/java/android/hardware/soundtrigger/SoundTriggerModule.java b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java new file mode 100644 index 0000000..776f85d --- /dev/null +++ b/core/java/android/hardware/soundtrigger/SoundTriggerModule.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.soundtrigger; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import java.lang.ref.WeakReference; +import java.util.UUID; + +/** + * The SoundTriggerModule provides APIs to control sound models and sound detection + * on a given sound trigger hardware module. + * + * @hide + */ +public class SoundTriggerModule { + private long mNativeContext; + + private int mId; + private NativeEventHandlerDelegate mEventHandlerDelegate; + + private static final int EVENT_RECOGNITION = 1; + private static final int EVENT_SERVICE_DIED = 2; + + SoundTriggerModule(int moduleId, SoundTrigger.StatusListener listener, Handler handler) { + mId = moduleId; + mEventHandlerDelegate = new NativeEventHandlerDelegate(listener, handler); + native_setup(new WeakReference<SoundTriggerModule>(this)); + } + private native void native_setup(Object module_this); + + @Override + protected void finalize() { + native_finalize(); + } + private native void native_finalize(); + + /** + * Detach from this module. The {@link SoundTrigger.StatusListener} callback will not be called + * anymore and associated resources will be released. + * */ + public native void detach(); + + /** + * Load a {@link SoundTrigger.SoundModel} to the hardware. A sound model must be loaded in + * order to start listening to a key phrase in this model. + * @param model The sound model to load. + * @param soundModelHandle an array of int where the sound model handle will be returned. + * @return - {@link SoundTrigger#STATUS_OK} in case of success + * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error + * - {@link SoundTrigger#STATUS_PERMISSION_DENIED} if the caller does not have + * system permission + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} if parameters are invalid + * - {@link SoundTrigger#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails + * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence + */ + public native int loadSoundModel(SoundTrigger.SoundModel model, int[] soundModelHandle); + + /** + * Unload a {@link SoundTrigger.SoundModel} and abort any pendiong recognition + * @param soundModelHandle The sound model handle + * @return - {@link SoundTrigger#STATUS_OK} in case of success + * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error + * - {@link SoundTrigger#STATUS_PERMISSION_DENIED} if the caller does not have + * system permission + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} if the sound model handle is invalid + * - {@link SoundTrigger#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails + */ + public native int unloadSoundModel(int soundModelHandle); + + /** + * Start listening to all key phrases in a {@link SoundTrigger.SoundModel}. + * Recognition must be restarted after each callback (success or failure) received on + * the {@link SoundTrigger.StatusListener}. + * @param soundModelHandle The sound model handle to start listening to + * @param data Opaque data for use by the implementation for this recognition + * @return - {@link SoundTrigger#STATUS_OK} in case of success + * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error + * - {@link SoundTrigger#STATUS_PERMISSION_DENIED} if the caller does not have + * system permission + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} if the sound model handle is invalid + * - {@link SoundTrigger#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails + * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence + */ + public native int startRecognition(int soundModelHandle, byte[] data); + + /** + * Stop listening to all key phrases in a {@link SoundTrigger.SoundModel} + * @param soundModelHandle The sound model handle to stop listening to + * @return - {@link SoundTrigger#STATUS_OK} in case of success + * - {@link SoundTrigger#STATUS_ERROR} in case of unspecified error + * - {@link SoundTrigger#STATUS_PERMISSION_DENIED} if the caller does not have + * system permission + * - {@link SoundTrigger#STATUS_NO_INIT} if the native service cannot be reached + * - {@link SoundTrigger#STATUS_BAD_VALUE} if the sound model handle is invalid + * - {@link SoundTrigger#STATUS_DEAD_OBJECT} if the binder transaction to the native + * service fails + * - {@link SoundTrigger#STATUS_INVALID_OPERATION} if the call is out of sequence + */ + public native int stopRecognition(int soundModelHandle); + + private class NativeEventHandlerDelegate { + private final Handler mHandler; + + NativeEventHandlerDelegate(final SoundTrigger.StatusListener listener, + Handler handler) { + // find the looper for our new event handler + Looper looper; + if (handler != null) { + looper = handler.getLooper(); + } else { + looper = Looper.myLooper(); + if (looper == null) { + looper = Looper.getMainLooper(); + } + } + + // construct the event handler with this looper + if (looper != null) { + // implement the event handler delegate + mHandler = new Handler(looper) { + @Override + public void handleMessage(Message msg) { + switch(msg.what) { + case EVENT_RECOGNITION: + if (listener != null) { + listener.onRecognition( + (SoundTrigger.RecognitionEvent)msg.obj); + } + break; + case EVENT_SERVICE_DIED: + if (listener != null) { + listener.onServiceDied(); + } + break; + default: + break; + } + } + }; + } else { + mHandler = null; + } + } + + Handler handler() { + return mHandler; + } + } + + @SuppressWarnings("unused") + private static void postEventFromNative(Object module_ref, + int what, int arg1, int arg2, Object obj) { + SoundTriggerModule module = (SoundTriggerModule)((WeakReference)module_ref).get(); + if (module == null) { + return; + } + + NativeEventHandlerDelegate delegate = module.mEventHandlerDelegate; + if (delegate != null) { + Handler handler = delegate.handler(); + if (handler != null) { + Message m = handler.obtainMessage(what, arg1, arg2, obj); + handler.sendMessage(m); + } + } + } +} + 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/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index a414421..b96f166 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -21,6 +21,7 @@ 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; @@ -34,15 +35,18 @@ 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; -import com.android.internal.util.Protocol; - /** * Class that answers queries about the state of network connectivity. It also * notifies applications when network connectivity changes. Get an instance @@ -57,13 +61,15 @@ import com.android.internal.util.Protocol; * 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. @@ -547,13 +553,12 @@ public class ConnectivityManager { * @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) { - // TODO - deprecate with: - // @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. } /** @@ -563,14 +568,13 @@ public class ConnectivityManager { * * <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() { - // TODO - deprecate with: - // @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. - return -1; + return TYPE_NONE; } /** @@ -716,7 +720,13 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * 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); @@ -725,7 +735,13 @@ public class ConnectivityManager { } } - /** {@hide} */ + /** + * 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); @@ -788,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; } } @@ -810,13 +851,212 @@ 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; + } + + /** + * Removes the NET_CAPABILITY_NOT_RESTRICTED capability from the given + * NetworkCapabilities object if all the capabilities it provides are + * typically provided by restricted networks. + * + * TODO: consider: + * - Moving to NetworkCapabilities + * - Renaming it to guessRestrictedCapability and make it set the + * restricted capability bit in addition to clearing it. + * @hide + */ + public static void maybeMarkCapabilitiesRestricted(NetworkCapabilities nc) { + for (Integer capability : nc.getNetworkCapabilities()) { + switch (capability.intValue()) { + case NetworkCapabilities.NET_CAPABILITY_CBS: + case NetworkCapabilities.NET_CAPABILITY_DUN: + case NetworkCapabilities.NET_CAPABILITY_EIMS: + case NetworkCapabilities.NET_CAPABILITY_FOTA: + case NetworkCapabilities.NET_CAPABILITY_IA: + case NetworkCapabilities.NET_CAPABILITY_IMS: + case NetworkCapabilities.NET_CAPABILITY_RCS: + case NetworkCapabilities.NET_CAPABILITY_XCAP: + continue; + default: + // At least one capability usually provided by unrestricted + // networks. Conclude that this network is unrestricted. + return; + } + } + // All the capabilities are typically provided by restricted networks. + // Conclude that this network is restricted. + nc.removeNetworkCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED); + } + + 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); + maybeMarkCapabilitiesRestricted(netCap); + 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); + maybeMarkCapabilitiesRestricted(netCap); + 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; + } } /** @@ -829,6 +1069,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); @@ -851,6 +1094,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(); @@ -918,34 +1163,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; } /** @@ -1332,13 +1561,13 @@ public class ConnectivityManager { } /** - * Report a problem network to the framework. This will cause the framework - * to evaluate the situation and try to fix any problems. Note that false - * may be subsequently ignored. + * 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 Network the application was attempting to use or null - * to indicate the current default network. - * {@hide} + * @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 { @@ -1358,6 +1587,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 { @@ -1374,6 +1604,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 { @@ -1393,6 +1624,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 { @@ -1630,9 +1862,16 @@ public class ConnectivityManager { } /** {@hide} */ - public void registerNetworkFactory(Messenger messenger) { + public void registerNetworkFactory(Messenger messenger, String name) { + try { + mService.registerNetworkFactory(messenger, name); + } catch (RemoteException e) { } + } + + /** {@hide} */ + public void unregisterNetworkFactory(Messenger messenger) { try { - mService.registerNetworkFactory(messenger); + mService.unregisterNetworkFactory(messenger); } catch (RemoteException e) { } } @@ -1645,11 +1884,10 @@ public class ConnectivityManager { } /** - * Interface for NetworkRequest callbacks. Used for notifications about network - * changes. - * @hide + * Base class for NetworkRequest callbacks. Used for notifications about network + * changes. Should be extended by applications wanting notifications. */ - public static class NetworkCallbacks { + public static class NetworkCallbackListener { /** @hide */ public static final int PRECHECK = 1; /** @hide */ @@ -1675,51 +1913,73 @@ public class ConnectivityManager { public void onPreCheck(NetworkRequest networkRequest, Network network) {} /** - * Called when the framework connects and has validated the new 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 framework is losing the network. Often paired with an - * 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 an onLost call or an onAvailable call for this - * network depending on if we lose or regain it. + * 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. Note applications should only request this callback - * if the application is willing to track the Available and Lost callbacks - * together, else the application may think it has no network when it - * really does (A Avail, B Avail, A Lost.. still have B). + * 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 LinkProperties. + * 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 releaseNetworkRequest call concludes and the registered callbacks will - * no longer be used. + * 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) {} } @@ -1743,14 +2003,16 @@ public class ConnectivityManager { 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 static class CallbackHandler extends Handler { - private final HashMap<NetworkRequest, NetworkCallbacks>mCallbackMap; + 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, NetworkCallbacks>callbackMap, + CallbackHandler(Looper looper, HashMap<NetworkRequest, NetworkCallbackListener>callbackMap, AtomicInteger refCount, ConnectivityManager cm) { super(looper); mCallbackMap = callbackMap; @@ -1764,7 +2026,7 @@ public class ConnectivityManager { switch (message.what) { case CALLBACK_PRECHECK: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { callbacks.onPreCheck(request, getNetwork(message)); } else { @@ -1774,7 +2036,7 @@ public class ConnectivityManager { } case CALLBACK_AVAILABLE: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { callbacks.onAvailable(request, getNetwork(message)); } else { @@ -1784,7 +2046,7 @@ public class ConnectivityManager { } case CALLBACK_LOSING: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { callbacks.onLosing(request, getNetwork(message), message.arg1); } else { @@ -1794,7 +2056,7 @@ public class ConnectivityManager { } case CALLBACK_LOST: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { callbacks.onLost(request, getNetwork(message)); } else { @@ -1804,7 +2066,7 @@ public class ConnectivityManager { } case CALLBACK_UNAVAIL: { NetworkRequest req = (NetworkRequest)message.obj; - NetworkCallbacks callbacks = null; + NetworkCallbackListener callbacks = null; synchronized(mCallbackMap) { callbacks = mCallbackMap.get(req); } @@ -1817,7 +2079,7 @@ public class ConnectivityManager { } case CALLBACK_CAP_CHANGED: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { Network network = getNetwork(message); NetworkCapabilities cap = mCm.getNetworkCapabilities(network); @@ -1830,7 +2092,7 @@ public class ConnectivityManager { } case CALLBACK_IP_CHANGED: { NetworkRequest request = getNetworkRequest(message); - NetworkCallbacks callbacks = getCallbacks(request); + NetworkCallbackListener callbacks = getCallbacks(request); if (callbacks != null) { Network network = getNetwork(message); LinkProperties lp = mCm.getLinkProperties(network); @@ -1843,7 +2105,7 @@ public class ConnectivityManager { } case CALLBACK_RELEASED: { NetworkRequest req = (NetworkRequest)message.obj; - NetworkCallbacks callbacks = null; + NetworkCallbackListener callbacks = null; synchronized(mCallbackMap) { callbacks = mCallbackMap.remove(req); } @@ -1864,13 +2126,17 @@ public class ConnectivityManager { getLooper().quit(); break; } + case EXPIRE_LEGACY_REQUEST: { + expireRequest((NetworkCapabilities)message.obj, message.arg1); + break; + } } } private NetworkRequest getNetworkRequest(Message msg) { return (NetworkRequest)(msg.obj); } - private NetworkCallbacks getCallbacks(NetworkRequest req) { + private NetworkCallbackListener getCallbacks(NetworkRequest req) { synchronized(mCallbackMap) { return mCallbackMap.get(req); } @@ -1878,7 +2144,7 @@ public class ConnectivityManager { private Network getNetwork(Message msg) { return new Network(msg.arg2); } - private NetworkCallbacks removeCallbacks(Message msg) { + private NetworkCallbackListener removeCallbacks(Message msg) { NetworkRequest req = (NetworkRequest)msg.obj; synchronized(mCallbackMap) { return mCallbackMap.remove(req); @@ -1893,7 +2159,7 @@ public class ConnectivityManager { HandlerThread callbackThread = new HandlerThread("ConnectivityManager"); callbackThread.start(); sCallbackHandler = new CallbackHandler(callbackThread.getLooper(), - sNetworkCallbacks, sCallbackRefCount, this); + sNetworkCallbackListener, sCallbackRefCount, this); } } } @@ -1907,18 +2173,21 @@ public class ConnectivityManager { } } - static final HashMap<NetworkRequest, NetworkCallbacks> sNetworkCallbacks = - new HashMap<NetworkRequest, NetworkCallbacks>(); + 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 somethingForNetwork(NetworkCapabilities need, - NetworkCallbacks networkCallbacks, int timeoutSec, int action) { + private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, + NetworkCallbackListener networkCallbackListener, int timeoutSec, int action, + int legacyType) { NetworkRequest networkRequest = null; - if (networkCallbacks == null) throw new IllegalArgumentException("null NetworkCallbacks"); + if (networkCallbackListener == null) { + throw new IllegalArgumentException("null NetworkCallbackListener"); + } if (need == null) throw new IllegalArgumentException("null NetworkCapabilities"); try { addCallbackListener(); @@ -1927,11 +2196,11 @@ public class ConnectivityManager { new Binder()); } else { networkRequest = mService.requestNetwork(need, new Messenger(sCallbackHandler), - timeoutSec, new Binder()); + timeoutSec, new Binder(), legacyType); } if (networkRequest != null) { - synchronized(sNetworkCallbacks) { - sNetworkCallbacks.put(networkRequest, networkCallbacks); + synchronized(sNetworkCallbackListener) { + sNetworkCallbackListener.put(networkRequest, networkCallbackListener); } } } catch (RemoteException e) {} @@ -1943,46 +2212,45 @@ public class ConnectivityManager { * 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 follwed by listening to the various - * callbacks described in {@link NetworkCallbacks}. The {@link Network} - * can be used by using the {@link bindSocketToNetwork}, - * {@link bindApplicationToNetwork} and {@link getAddrInfoOnNetwork} functions. + * {@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 networkCallbacks 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 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. - * @hide */ public NetworkRequest requestNetwork(NetworkCapabilities need, - NetworkCallbacks networkCallbacks) { - return somethingForNetwork(need, networkCallbacks, 0, REQUEST); + 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, but if a suitable network is not found - * within the given time (in Seconds) the {@link NetworkCallbacks#unavailable} - * callback is called. The request must still be released normally by - * calling {@link releaseNetworkRequest}. + * 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 networkCallbacks The callbacks to be utilized for this request. Note + * @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 NetworkCallbacks#unavailable} is called. + * before {@link NetworkCallbackListener#unavailable} is called. * @return A {@link NetworkRequest} object identifying the request. * @hide */ public NetworkRequest requestNetwork(NetworkCapabilities need, - NetworkCallbacks networkCallbacks, int timeoutSec) { - return somethingForNetwork(need, networkCallbacks, timeoutSec, REQUEST); + NetworkCallbackListener networkCallbackListener, int timeoutSec) { + return sendRequestForNetwork(need, networkCallbackListener, timeoutSec, REQUEST, + TYPE_NONE); } /** @@ -1993,36 +2261,52 @@ public class ConnectivityManager { 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, but instead of {@link NetworkCallbacks} - * a {@link PendingIntent} is used. This means the request may outlive the - * calling application and get called back when a suitable network is found. + * 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_CAPABILTIES} containing + * 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 NetworkCallbacks} based request before completing the processing of the + * {@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 - * replace by this one, effectively releasing the previous {@link NetworkRequest}. + * replaced by this one, effectively releasing the previous {@link NetworkRequest}. * <p> - * The request may be released normally by calling {@link releaseNetworkRequest}. + * The request may be released normally by calling {@link #releaseNetworkRequest}. * - * @param need {@link NetworkCapabilties} required by this request. + * @param need {@link NetworkCapabilities} required by this request. * @param operation Action to perform when the network is available (corresponds - * to the {@link NetworkCallbacks#onAvailable} call. Typically + * to the {@link NetworkCallbackListener#onAvailable} call. Typically * comes from {@link PendingIntent#getBroadcast}. * @return A {@link NetworkRequest} object identifying the request. - * @hide */ public NetworkRequest requestNetwork(NetworkCapabilities need, PendingIntent operation) { try { @@ -2035,28 +2319,27 @@ public class ConnectivityManager { * 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}. + * {@link #releaseNetworkRequest}. * * @param need {@link NetworkCapabilities} required by this request. - * @param networkCallbacks The {@link NetworkCallbacks} to be called as suitable + * @param networkCallbackListener The {@link NetworkCallbackListener} to be called as suitable * networks change state. * @return A {@link NetworkRequest} object identifying the request. - * @hide */ public NetworkRequest listenForNetwork(NetworkCapabilities need, - NetworkCallbacks networkCallbacks) { - return somethingForNetwork(need, networkCallbacks, 0, LISTEN); + NetworkCallbackListener networkCallbackListener) { + return sendRequestForNetwork(need, networkCallbackListener, 0, LISTEN, TYPE_NONE); } /** - * Releases a {NetworkRequest} generated either through a {@link requestNetwork} - * or a {@link listenForNetwork} call. The {@link NetworkCallbacks} given in the - * earlier call may continue receiving calls until the {@link NetworkCallbacks#onReleased} - * function is called, signifiying the end of the request. + * 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}. - * @hide + * {@link #requestNetwork} or {@link #listenForNetwork}. */ public void releaseNetworkRequest(NetworkRequest networkRequest) { if (networkRequest == null) throw new IllegalArgumentException("null NetworkRequest"); diff --git a/core/java/android/net/ConnectivityServiceProtocol.java b/core/java/android/net/ConnectivityServiceProtocol.java deleted file mode 100644 index 74096b4..0000000 --- a/core/java/android/net/ConnectivityServiceProtocol.java +++ /dev/null @@ -1,70 +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.net; - -import static com.android.internal.util.Protocol.BASE_CONNECTIVITY_SERVICE; - -/** - * Describes the Internal protocols used to communicate with ConnectivityService. - * @hide - */ -public class ConnectivityServiceProtocol { - - private static final int BASE = BASE_CONNECTIVITY_SERVICE; - - private ConnectivityServiceProtocol() {} - - /** - * This is a contract between ConnectivityService and various bearers. - * A NetworkFactory is an abstract entity that creates NetworkAgent objects. - * The bearers register with ConnectivityService using - * ConnectivityManager.registerNetworkFactory, where they pass in a Messenger - * to be used to deliver the following Messages. - */ - public static class NetworkFactoryProtocol { - private NetworkFactoryProtocol() {} - /** - * 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; - } -} diff --git a/core/java/android/net/EthernetDataTracker.java b/core/java/android/net/EthernetDataTracker.java deleted file mode 100644 index c1afc9b..0000000 --- a/core/java/android/net/EthernetDataTracker.java +++ /dev/null @@ -1,427 +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(); - mNetworkCapabilities = new NetworkCapabilities(); - } - - 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); - } - - /** - * 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 885b8b6..5f1ff3e 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -74,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); @@ -153,13 +150,15 @@ interface IConnectivityManager void setAirplaneMode(boolean enable); - void registerNetworkFactory(in Messenger messenger); + 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); + in Messenger messenger, int timeoutSec, in IBinder binder, int legacy); NetworkRequest pendingRequestForNetwork(in NetworkCapabilities networkCapabilities, in PendingIntent operation); @@ -171,4 +170,6 @@ interface IConnectivityManager 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/LinkProperties.java b/core/java/android/net/LinkProperties.java index 0a09fcb..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) { /* 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/Network.java b/core/java/android/net/Network.java index ac1289b..64516e6 100644 --- a/core/java/android/net/Network.java +++ b/core/java/android/net/Network.java @@ -16,24 +16,43 @@ 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 the Network. - * @hide + * 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; } @@ -64,6 +83,140 @@ public class Network implements Parcelable { 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; @@ -84,4 +237,14 @@ public class Network implements Parcelable { 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 index 4b85398..7e8b1f1 100644 --- a/core/java/android/net/NetworkAgent.java +++ b/core/java/android/net/NetworkAgent.java @@ -24,84 +24,39 @@ 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; +import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; /** - * A Utility class for handling NetworkRequests. - * - * Created by bearer-specific code to handle tracking requests, scores, - * network data and handle communicating with ConnectivityService. Two - * abstract methods: connect and disconnect are used to act on the - * underlying bearer code. Connect is called when we have a NetworkRequest - * and our score is better than the current handling network's score, while - * disconnect is used when ConnectivityService requests a disconnect. + * 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). The bearer - * code should pass its NetworkAgents the NetworkRequests each NetworkAgent - * can handle, demultiplexing for different network types. The bearer code - * can also filter out requests it can never handle. + * perhaps connections with different SSID or P2P for Wi-Fi). * - * Each NetworkAgent needs to be given a score and NetworkCapabilities for - * their potential network. While disconnected, the NetworkAgent will check - * each time its score changes or a NetworkRequest changes to see if - * the NetworkAgent can provide a higher scored network for a NetworkRequest - * that the NetworkAgent's NetworkCapabilties can satisfy. This condition will - * trigger a connect request via connect(). After connection, connection data - * should be given to the NetworkAgent by the bearer, including LinkProperties - * NetworkCapabilties and NetworkInfo. After that the NetworkAgent will register - * with ConnectivityService and forward the data on. * @hide */ public abstract class NetworkAgent extends Handler { - private final SparseArray<NetworkRequestAndScore> mNetworkRequests = new SparseArray<>(); - private boolean mConnectionRequested = false; - - private AsyncChannel mAsyncChannel; + private volatile AsyncChannel mAsyncChannel; private final String LOG_TAG; private static final boolean DBG = true; - // TODO - this class shouldn't cache data or it runs the risk of getting out of sync - // Make the API require each of these when any is updated so we have the data we need, - // without caching. - private LinkProperties mLinkProperties; - private NetworkInfo mNetworkInfo; - private NetworkCapabilities mNetworkCapabilities; - private int mNetworkScore; - private boolean mRegistered = false; + private static final boolean VDBG = true; private final Context mContext; - private AtomicBoolean mHasRequests = new AtomicBoolean(false); - - // TODO - add a name member for logging purposes. - - protected final Object mLockObj = new Object(); - + private final ArrayList<Message>mPreConnectedQueue = new ArrayList<Message>(); private static final int BASE = Protocol.BASE_NETWORK_AGENT; /** - * Sent by self to queue up a new/modified request. - * obj = NetworkRequestAndScore - */ - private static final int CMD_ADD_REQUEST = BASE + 1; - - /** - * Sent by self to queue up the removal of a request. - * obj = NetworkRequest - */ - private static final int CMD_REMOVE_REQUEST = BASE + 2; - - /** * 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 + 3; + public static final int CMD_SUSPECT_BAD = BASE; /** * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to @@ -109,84 +64,63 @@ public abstract class NetworkAgent extends Handler { * Sent when the NetworkInfo changes, mainly due to change of state. * obj = NetworkInfo */ - public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 4; + 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 + 5; + 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 + 6; + public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3; /** * Sent by the NetworkAgent to ConnectivityService to pass the current * network score. - * arg1 = network score int + * obj = network score Integer */ - public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 7; + public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4; - public NetworkAgent(Looper looper, Context context, String logTag) { + public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni, + NetworkCapabilities nc, LinkProperties lp, int score) { super(looper); LOG_TAG = logTag; mContext = context; - } - - /** - * When conditions are right, register with ConnectivityService. - * Connditions include having a well defined network and a request - * that justifies it. The NetworkAgent will remain registered until - * disconnected. - * TODO - this should have all data passed in rather than caching - */ - private void registerSelf() { - synchronized(mLockObj) { - if (!mRegistered && mConnectionRequested && - mNetworkInfo != null && mNetworkInfo.isConnected() && - mNetworkCapabilities != null && - mLinkProperties != null && - mNetworkScore != 0) { - if (DBG) log("Registering NetworkAgent"); - mRegistered = true; - ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService( - Context.CONNECTIVITY_SERVICE); - cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(mNetworkInfo), - new LinkProperties(mLinkProperties), - new NetworkCapabilities(mNetworkCapabilities), mNetworkScore); - } else if (DBG && !mRegistered) { - String err = "Not registering due to "; - if (mConnectionRequested == false) err += "no Connect requested "; - if (mNetworkInfo == null) err += "null NetworkInfo "; - if (mNetworkInfo != null && mNetworkInfo.isConnected() == false) { - err += "NetworkInfo disconnected "; - } - if (mLinkProperties == null) err += "null LinkProperties "; - if (mNetworkCapabilities == null) err += "null NetworkCapabilities "; - if (mNetworkScore == 0) err += "null NetworkScore"; - log(err); - } + 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: { - synchronized (mLockObj) { - if (mAsyncChannel != null) { - log("Received new connection while already connected!"); - } else { - if (DBG) log("NetworkAgent fully connected"); - mAsyncChannel = new AsyncChannel(); - mAsyncChannel.connected(null, this, msg.replyTo); - mAsyncChannel.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, - AsyncChannel.STATUS_SUCCESSFUL); + 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; @@ -198,198 +132,69 @@ public abstract class NetworkAgent extends Handler { } case AsyncChannel.CMD_CHANNEL_DISCONNECTED: { if (DBG) log("NetworkAgent channel lost"); - disconnect(); - clear(); + // let the client know CS is done with us. + unwanted(); + synchronized (mPreConnectedQueue) { + mAsyncChannel = null; + } break; } case CMD_SUSPECT_BAD: { log("Unhandled Message " + msg); break; } - case CMD_ADD_REQUEST: { - handleAddRequest(msg); - break; - } - case CMD_REMOVE_REQUEST: { - handleRemoveRequest(msg); - break; - } - } - } - - private void clear() { - synchronized(mLockObj) { - mNetworkRequests.clear(); - mHasRequests.set(false); - mConnectionRequested = false; - mAsyncChannel = null; - mRegistered = false; - } - } - - private static class NetworkRequestAndScore { - NetworkRequest req; - int score; - - NetworkRequestAndScore(NetworkRequest networkRequest, int score) { - req = networkRequest; - this.score = score; - } - } - - private void handleAddRequest(Message msg) { - NetworkRequestAndScore n = (NetworkRequestAndScore)msg.obj; - // replaces old request, updating score - mNetworkRequests.put(n.req.requestId, n); - mHasRequests.set(true); - evalScores(); - } - - private void handleRemoveRequest(Message msg) { - NetworkRequest networkRequest = (NetworkRequest)msg.obj; - - if (mNetworkRequests.get(networkRequest.requestId) != null) { - mNetworkRequests.remove(networkRequest.requestId); - if (mNetworkRequests.size() == 0) mHasRequests.set(false); - evalScores(); } } - /** - * called to go through our list of requests and see if we're - * good enough to try connecting. - * - * Only does connects - we disconnect when requested via - * CMD_CHANNEL_DISCONNECTED, generated by either a loss of connection - * between modules (bearer or ConnectivityService dies) or more commonly - * when the NetworkInfo reports to ConnectivityService it is disconnected. - */ - private void evalScores() { - if (mConnectionRequested) { - // already trying - return; - } - for (int i=0; i < mNetworkRequests.size(); i++) { - int score = mNetworkRequests.valueAt(i).score; - if (score < mNetworkScore) { - // have a request that has a lower scored network servicing it - // (or no network) than we could provide, so lets connect! - mConnectionRequested = true; - connect(); - return; + 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); } } } - public void addNetworkRequest(NetworkRequest networkRequest, int score) { - if (DBG) log("adding NetworkRequest " + networkRequest + " with score " + score); - sendMessage(obtainMessage(CMD_ADD_REQUEST, - new NetworkRequestAndScore(networkRequest, score))); - } - - public void removeNetworkRequest(NetworkRequest networkRequest) { - if (DBG) log("removing NetworkRequest " + networkRequest); - sendMessage(obtainMessage(CMD_REMOVE_REQUEST, networkRequest)); - } - /** * Called by the bearer code when it has new LinkProperties data. - * If we're a registered NetworkAgent, this new data will get forwarded on, - * otherwise we store a copy in anticipation of registering. This call - * may also prompt registration if it causes the NetworkAgent to meet - * the conditions (fully configured, connected, satisfys a request and - * has sufficient score). */ public void sendLinkProperties(LinkProperties linkProperties) { - linkProperties = new LinkProperties(linkProperties); - synchronized(mLockObj) { - mLinkProperties = linkProperties; - if (mAsyncChannel != null) { - mAsyncChannel.sendMessage(EVENT_NETWORK_PROPERTIES_CHANGED, linkProperties); - } else { - registerSelf(); - } - } + queueOrSendMessage(EVENT_NETWORK_PROPERTIES_CHANGED, new LinkProperties(linkProperties)); } /** * Called by the bearer code when it has new NetworkInfo data. - * If we're a registered NetworkAgent, this new data will get forwarded on, - * otherwise we store a copy in anticipation of registering. This call - * may also prompt registration if it causes the NetworkAgent to meet - * the conditions (fully configured, connected, satisfys a request and - * has sufficient score). */ public void sendNetworkInfo(NetworkInfo networkInfo) { - networkInfo = new NetworkInfo(networkInfo); - synchronized(mLockObj) { - mNetworkInfo = networkInfo; - if (mAsyncChannel != null) { - mAsyncChannel.sendMessage(EVENT_NETWORK_INFO_CHANGED, networkInfo); - } else { - registerSelf(); - } - } + queueOrSendMessage(EVENT_NETWORK_INFO_CHANGED, new NetworkInfo(networkInfo)); } /** * Called by the bearer code when it has new NetworkCapabilities data. - * If we're a registered NetworkAgent, this new data will get forwarded on, - * otherwise we store a copy in anticipation of registering. This call - * may also prompt registration if it causes the NetworkAgent to meet - * the conditions (fully configured, connected, satisfys a request and - * has sufficient score). - * Note that if these capabilities make the network non-useful, - * ConnectivityServce will tear this network down. */ public void sendNetworkCapabilities(NetworkCapabilities networkCapabilities) { - networkCapabilities = new NetworkCapabilities(networkCapabilities); - synchronized(mLockObj) { - mNetworkCapabilities = networkCapabilities; - if (mAsyncChannel != null) { - mAsyncChannel.sendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED, networkCapabilities); - } else { - registerSelf(); - } - } - } - - public NetworkCapabilities getNetworkCapabilities() { - synchronized(mLockObj) { - return new NetworkCapabilities(mNetworkCapabilities); - } + queueOrSendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED, + new NetworkCapabilities(networkCapabilities)); } /** * Called by the bearer code when it has a new score for this network. - * If we're a registered NetworkAgent, this new data will get forwarded on, - * otherwise we store a copy. */ - public synchronized void sendNetworkScore(int score) { - synchronized(mLockObj) { - mNetworkScore = score; - evalScores(); - if (mAsyncChannel != null) { - mAsyncChannel.sendMessage(EVENT_NETWORK_SCORE_CHANGED, mNetworkScore); - } else { - registerSelf(); - } - } - } - - public boolean hasRequests() { - return mHasRequests.get(); + public void sendNetworkScore(int score) { + queueOrSendMessage(EVENT_NETWORK_SCORE_CHANGED, new Integer(score)); } - public boolean isConnectionRequested() { - synchronized(mLockObj) { - return mConnectionRequested; - } - } - - - abstract protected void connect(); - abstract protected void disconnect(); + /** + * 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.java b/core/java/android/net/NetworkCapabilities.java index 8005e5c..35274f1 100644 --- a/core/java/android/net/NetworkCapabilities.java +++ b/core/java/android/net/NetworkCapabilities.java @@ -30,13 +30,31 @@ import java.util.Map.Entry; import java.util.Set; /** - * A class representing the capabilities of a network - * @hide + * 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 @@ -45,28 +63,99 @@ public final class NetworkCapabilities implements Parcelable { private long mNetworkCapabilities = (1 << NET_CAPABILITY_NOT_RESTRICTED); /** - * Values for NetworkCapabilities. Roughly matches/extends deprecated - * ConnectivityManager TYPE_* + * 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; - /** Set by default */ + + /** + * 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) { @@ -74,6 +163,12 @@ public final class NetworkCapabilities implements Parcelable { } 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) { @@ -81,9 +176,23 @@ public final class NetworkCapabilities implements Parcelable { } 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) { @@ -124,31 +233,74 @@ public final class NetworkCapabilities implements Parcelable { private long mTransportTypes; /** - * Values for TransportType + * 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; @@ -175,15 +327,58 @@ public final class NetworkCapabilities implements Parcelable { 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; } @@ -243,19 +438,6 @@ public final class NetworkCapabilities implements Parcelable { (mLinkDownBandwidthKbps * 13)); } - public NetworkCapabilities() { - } - - public NetworkCapabilities(NetworkCapabilities nc) { - if (nc != null) { - mNetworkCapabilities = nc.mNetworkCapabilities; - mTransportTypes = nc.mTransportTypes; - mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps; - mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps; - } - } - - // Parcelable public int describeContents() { return 0; } 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 9e656ee..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; diff --git a/core/java/android/net/NetworkRequest.java b/core/java/android/net/NetworkRequest.java index b3ae3f5..47377e9 100644 --- a/core/java/android/net/NetworkRequest.java +++ b/core/java/android/net/NetworkRequest.java @@ -22,11 +22,19 @@ import android.os.Parcelable; import java.util.concurrent.atomic.AtomicInteger; /** - * @hide + * 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 NetworkCapabilities that define this request + * 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; @@ -34,46 +42,33 @@ public class NetworkRequest implements Parcelable { * 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. - * TODO - make sure this input is checked whenever a NR is passed in a public API + * @hide */ public final int requestId; /** - * Set for legacy requests and the default. + * Set for legacy requests and the default. Set to TYPE_NONE for none. * Causes CONNECTIVITY_ACTION broadcasts to be sent. * @hide */ - public final boolean needsBroadcasts; - - private static final AtomicInteger sNextRequestId = new AtomicInteger(1); - - /** - * @hide - */ - public NetworkRequest(NetworkCapabilities nc) { - this(nc, false, sNextRequestId.getAndIncrement()); - } + public final int legacyType; /** * @hide */ - public NetworkRequest(NetworkCapabilities nc, boolean needsBroadcasts) { - this(nc, needsBroadcasts, sNextRequestId.getAndIncrement()); + public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId) { + requestId = rId; + networkCapabilities = nc; + this.legacyType = legacyType; } /** * @hide */ - private NetworkRequest(NetworkCapabilities nc, boolean needsBroadcasts, int rId) { - requestId = rId; - networkCapabilities = nc; - this.needsBroadcasts = needsBroadcasts; - } - public NetworkRequest(NetworkRequest that) { networkCapabilities = new NetworkCapabilities(that.networkCapabilities); requestId = that.requestId; - needsBroadcasts = that.needsBroadcasts; + this.legacyType = that.legacyType; } // implement the Parcelable interface @@ -82,16 +77,16 @@ public class NetworkRequest implements Parcelable { } public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(networkCapabilities, flags); - dest.writeInt(needsBroadcasts ? 1 : 0); + 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); - boolean needsBroadcasts = (in.readInt() == 1); + int legacyType = in.readInt(); int requestId = in.readInt(); - NetworkRequest result = new NetworkRequest(nc, needsBroadcasts, requestId); + NetworkRequest result = new NetworkRequest(nc, legacyType, requestId); return result; } public NetworkRequest[] newArray(int size) { @@ -100,14 +95,14 @@ public class NetworkRequest implements Parcelable { }; public String toString() { - return "NetworkRequest [ id=" + requestId + ", needsBroadcasts=" + needsBroadcasts + + 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.needsBroadcasts == this.needsBroadcasts && + return (that.legacyType == this.legacyType && that.requestId == this.requestId && ((that.networkCapabilities == null && this.networkCapabilities == null) || (that.networkCapabilities != null && @@ -115,7 +110,7 @@ public class NetworkRequest implements Parcelable { } public int hashCode() { - return requestId + (needsBroadcasts ? 1013 : 2026) + + return requestId + (legacyType * 1013) + (networkCapabilities.hashCode() * 1051); } } 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/ProxyDataTracker.java b/core/java/android/net/ProxyDataTracker.java index 4973b3d..0340e7e 100644 --- a/core/java/android/net/ProxyDataTracker.java +++ b/core/java/android/net/ProxyDataTracker.java @@ -48,6 +48,7 @@ public class ProxyDataTracker extends BaseNetworkStateTracker { // TODO: investigate how to get these DNS addresses from the system. private static final String DNS1 = "8.8.8.8"; private static final String DNS2 = "8.8.4.4"; + private static final String INTERFACE_NAME = "ifb0"; private static final String REASON_ENABLED = "enabled"; private static final String REASON_DISABLED = "disabled"; private static final String REASON_PROXY_DOWN = "proxy_down"; @@ -107,10 +108,11 @@ public class ProxyDataTracker extends BaseNetworkStateTracker { mNetworkCapabilities = new NetworkCapabilities(); mNetworkInfo.setIsAvailable(true); try { - mLinkProperties.addDns(InetAddress.getByName(DNS1)); - mLinkProperties.addDns(InetAddress.getByName(DNS2)); + mLinkProperties.addDns(InetAddress.getByName(DNS1)); + mLinkProperties.addDns(InetAddress.getByName(DNS2)); + mLinkProperties.setInterfaceName(INTERFACE_NAME); } catch (UnknownHostException e) { - Log.e(TAG, "Could not add DNS address", e); + Log.e(TAG, "Could not add DNS address", e); } } diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java index 991d9da..7ea6bae 100644 --- a/core/java/android/net/ProxyInfo.java +++ b/core/java/android/net/ProxyInfo.java @@ -155,11 +155,10 @@ public class ProxyInfo implements Parcelable { mHost = source.getHost(); mPort = source.getPort(); mPacFileUrl = source.mPacFileUrl; - if (mPacFileUrl == null) { - mPacFileUrl = Uri.EMPTY; - } mExclusionList = source.getExclusionListAsString(); mParsedExclusionList = source.mParsedExclusionList; + } else { + mPacFileUrl = Uri.EMPTY; } } 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 4857533..e627d49 100644 --- a/core/java/android/os/BatteryStats.java +++ b/core/java/android/os/BatteryStats.java @@ -31,7 +31,6 @@ import android.telephony.SignalStrength; import android.text.format.DateFormat; import android.util.Printer; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.TimeUtils; import com.android.internal.os.BatterySipper; @@ -532,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 @@ -601,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. @@ -622,8 +623,11 @@ 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); @@ -635,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. @@ -887,6 +893,11 @@ public abstract class BatteryStats implements Parcelable { 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]; } @@ -920,6 +931,14 @@ public abstract class BatteryStats implements Parcelable { } } + /** + * 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(); @@ -997,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. * @@ -1157,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" }; /** @@ -1630,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(); @@ -1699,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]; @@ -2092,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:"); @@ -2139,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; @@ -2409,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); @@ -3004,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 { @@ -3190,6 +3236,7 @@ public abstract class BatteryStats implements Parcelable { } pw.println(); oldState = rec.states; + oldState2 = rec.states2; } } } @@ -3264,23 +3311,28 @@ public abstract class BatteryStats implements Parcelable { if (rec.time >= histStart) { if (histStart >= 0 && !printed) { if (rec.cmd == HistoryItem.CMD_CURRENT_TIME - || rec.cmd == HistoryItem.CMD_RESET) { + || 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; - if (checkin) { - pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); - pw.print(HISTORY_DATA); pw.print(','); - } hprinter.printNextItem(pw, rec, baseTime, checkin, (flags&DUMP_VERBOSE) != 0); rec.cmd = cmd; } if (tracker != null) { - int oldCode = rec.eventCode; - HistoryTag oldTag = rec.eventTag; + 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 @@ -3296,27 +3348,24 @@ public abstract class BatteryStats implements Parcelable { rec.eventTag.string = ent.getKey(); rec.eventTag.uid = uids.keyAt(j); rec.eventTag.poolIdx = uids.valueAt(j); - if (checkin) { - pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); - pw.print(HISTORY_DATA); pw.print(','); - } hprinter.printNextItem(pw, rec, baseTime, checkin, (flags&DUMP_VERBOSE) != 0); + rec.wakeReasonTag = null; + rec.wakelockTag = null; } } } - rec.eventCode = oldCode; - rec.eventTag = oldTag; + rec.eventCode = oldEventCode; + rec.eventTag = oldEventTag; tracker = null; } } - if (checkin) { - pw.print(BATTERY_STATS_CHECKIN_VERSION); pw.print(','); - pw.print(HISTORY_DATA); pw.print(','); - } hprinter.printNextItem(pw, rec, baseTime, checkin, (flags&DUMP_VERBOSE) != 0); - } else if (rec.eventCode != HistoryItem.EVENT_NONE) { + } 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(); } @@ -3325,6 +3374,7 @@ public abstract class BatteryStats implements Parcelable { } } if (histStart >= 0) { + commitCurrentHistoryBatchLocked(); pw.print(checkin ? "NEXT: " : " NEXT: "); pw.println(lastTime+1); } } 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..975bfc2 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); } @@ -288,6 +292,17 @@ public class Environment { } /** + * Returns the config directory for a user. This is for use by system services to store files + * relating to the user which should be readable by any app running as that user. + * + * @hide + */ + public static File getUserConfigDirectory(int userId) { + return new File(new File(new File( + getDataDirectory(), "misc"), "user"), Integer.toString(userId)); + } + + /** * Returns whether the Encrypted File System feature is enabled on the device or not. * @return <code>true</code> if Encrypted File System feature is enabled, <code>false</code> * if disabled. 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/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl index 6c7b08d..658180b 100644 --- a/core/java/android/os/IPowerManager.aidl +++ b/core/java/android/os/IPowerManager.aidl @@ -38,9 +38,10 @@ 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(); + boolean isPowerSaveMode(); void reboot(boolean confirm, String reason, boolean wait); void shutdown(boolean confirm, boolean wait); 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 5b2c8db..92e80a5 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.SdkConstant; import android.content.Context; import android.util.Log; @@ -321,6 +322,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; @@ -500,8 +507,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) { } } @@ -672,6 +686,30 @@ public final class PowerManager { } /** + * Returns true if the device is currently in power save mode. When in this mode, + * applications should reduce their functionality in order to conserve battery as + * much as possible. You can monitor for changes to this state with + * {@link #ACTION_POWER_SAVE_MODE_CHANGED}. + * + * @return Returns true if currently in low power mode, else false. + */ + public boolean isPowerSaveMode() { + try { + return mService.isPowerSaveMode(); + } catch (RemoteException e) { + return false; + } + } + + /** + * Intent that is broadcast when the state of {@link #isPowerSaveMode()} changes. + * This broadcast is only sent to registered receivers. + */ + @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) + public static final String ACTION_POWER_SAVE_MODE_CHANGED + = "android.os.action.POWER_SAVE_MODE_CHANGED"; + + /** * A wake lock is a mechanism to indicate that your application needs * to have the device stay on. * <p> 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..86c749a 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. @@ -146,6 +156,12 @@ public class Process { public static final int LAST_ISOLATED_UID = 99999; /** + * Defines the gid shared by all applications running under the same profile. + * @hide + */ + public static final int SHARED_USER_GID = 9997; + + /** * First gid for applications to share resources. Used when forward-locking * is enabled but all UserHandles need to be able to read the resources. * @hide @@ -338,8 +354,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 +373,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 +406,7 @@ public class Process { return abiList.contains(abi); } - void close() { + public void close() { try { socket.close(); } catch (IOException ex) { @@ -503,27 +492,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 +661,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 +679,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/UserHandle.java b/core/java/android/os/UserHandle.java index 6e693a4..914c170 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -145,6 +145,14 @@ public final class UserHandle implements Parcelable { } /** + * Returns the gid shared between all apps with this userId. + * @hide + */ + public static final int getUserGid(int userId) { + return getUid(userId, Process.SHARED_USER_GID); + } + + /** * Returns the shared app gid for a given uid or appId. * @hide */ diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 312cdbe..f7a89ba 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 */ @@ -679,16 +690,45 @@ public class UserManager { } } + /** + * If the target user is a managed profile of the calling user or the caller + * is itself a managed profile, then this returns a drawable to use as a small + * icon to include in a view to distinguish it from the original icon. + * + * @param user The target user. + * @return the drawable or null if no drawable is required. + * @hide + */ + public Drawable getBadgeForUser(UserHandle user) { + UserInfo userInfo = getUserIfProfile(user.getIdentifier()); + if (userInfo != null && userInfo.isManagedProfile()) { + return Resources.getSystem().getDrawable( + com.android.internal.R.drawable.ic_corp_badge); + } + return null; + } + private int getBadgeResIdForUser(int userHandle) { // Return the framework-provided badge. + UserInfo userInfo = getUserIfProfile(userHandle); + if (userInfo != null && userInfo.isManagedProfile()) { + return com.android.internal.R.drawable.ic_corp_icon_badge; + } + return 0; + } + + /** + * @return UserInfo for userHandle if it exists and is a profile of the current + * user or null. + */ + private UserInfo getUserIfProfile(int userHandle) { List<UserInfo> userProfiles = getProfiles(getUserHandle()); for (UserInfo user : userProfiles) { - if (user.id == userHandle - && user.isManagedProfile()) { - return com.android.internal.R.drawable.ic_corp_badge; + if (user.id == userHandle) { + return user; } } - return 0; + return null; } private Drawable getMergedDrawable(Drawable icon, Drawable badge) { 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/Contacts.java b/core/java/android/provider/Contacts.java index 9e2aacd..d4c5cfb 100644 --- a/core/java/android/provider/Contacts.java +++ b/core/java/android/provider/Contacts.java @@ -58,7 +58,7 @@ public class Contacts { @Deprecated public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY); - /** + /** * Signifies an email address row that is stored in the ContactMethods table * @deprecated see {@link android.provider.ContactsContract} */ @@ -337,7 +337,7 @@ public class Contacts { * @deprecated see {@link android.provider.ContactsContract} */ @Deprecated - public static final class People implements BaseColumns, SyncConstValue, PeopleColumns, + public static final class People implements BaseColumns, PeopleColumns, PhonesColumns, PresenceColumns { /** * no public constructor since this is a utility class @@ -790,7 +790,7 @@ public class Contacts { */ @Deprecated public static final class Groups - implements BaseColumns, SyncConstValue, GroupsColumns { + implements BaseColumns, GroupsColumns { /** * no public constructor since this is a utility class */ @@ -1864,7 +1864,7 @@ public class Contacts { * @deprecated see {@link android.provider.ContactsContract} */ @Deprecated - public static final class Photos implements BaseColumns, PhotosColumns, SyncConstValue { + public static final class Photos implements BaseColumns, PhotosColumns { /** * no public constructor since this is a utility class */ @@ -2199,7 +2199,7 @@ public class Contacts { } /** The action code to use when adding a contact - * @deprecated see {@link android.provider.ContactsContract} + * @deprecated see {@link android.provider.ContactsContract} */ @Deprecated public static final String ACTION = ContactsContract.Intents.Insert.ACTION; diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 11678a6..ba66e65 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -184,9 +184,9 @@ public final class ContactsContract { public static final String DEFERRED_SNIPPETING_QUERY = "deferred_snippeting_query"; /** - * A boolean parameter for {@link CommonDataKinds.Phone#CONTENT_URI}, - * {@link CommonDataKinds.Email#CONTENT_URI}, and - * {@link CommonDataKinds.StructuredPostal#CONTENT_URI}. + * A boolean parameter for {@link CommonDataKinds.Phone#CONTENT_URI Phone.CONTENT_URI}, + * {@link CommonDataKinds.Email#CONTENT_URI Email.CONTENT_URI}, and + * {@link CommonDataKinds.StructuredPostal#CONTENT_URI StructuredPostal.CONTENT_URI}. * This enables a content provider to remove duplicate entries in results. */ public static final String REMOVE_DUPLICATE_ENTRIES = "remove_duplicate_entries"; @@ -244,6 +244,9 @@ public final class ContactsContract { public static final String KEY_AUTHORIZED_URI = "authorized_uri"; } + /* + * @hide + */ public static final class Preferences { /** @@ -808,6 +811,7 @@ public final class ContactsContract { * The position at which the contact is pinned. If {@link PinnedPositions#UNPINNED}, * the contact is not pinned. Also see {@link PinnedPositions}. * <P>Type: INTEGER </P> + * @hide */ public static final String PINNED = "pinned"; @@ -903,8 +907,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 +1156,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 +1165,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 +1190,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 +1197,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"; } @@ -7766,6 +7779,8 @@ public final class ContactsContract { * {@link PinnedPositions#STAR_WHEN_PINNING} to true to force all pinned and unpinned * contacts to be automatically starred and unstarred. * </p> + * + * @hide */ public static final class PinnedPositions { diff --git a/core/java/android/provider/DocumentsContract.java b/core/java/android/provider/DocumentsContract.java index 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 3cf51b4..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) * @@ -4431,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. @@ -4525,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 @@ -5029,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 @@ -5311,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. @@ -6194,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 @@ -6207,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); } @@ -6229,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 5ffffb5..0000000 --- a/core/java/android/provider/TvContract.java +++ /dev/null @@ -1,673 +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 android.tv.TvInputService; - -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#COLUMN_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#COLUMN_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#COLUMN_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#COLUMN_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 COLUMN_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). */ - public static final int TYPE_ATSC_T = 0x00030000; - - /** The channel type for ATSC (cable). */ - public static final int TYPE_ATSC_C = 0x00030200; - - /** The channel type for ATSC-M/H (mobile/handheld). */ - public static final int TYPE_ATSC_M_H = 0x00030200; - - /** 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; - - /** A generic service type. */ - public static final int SERVICE_TYPE_OTHER = 0x0; - - /** The service type for regular TV channels. */ - public static final int SERVICE_TYPE_TV = 0x1; - - /** The service type for radio channels. */ - public static final int SERVICE_TYPE_RADIO = 0x2; - - /** - * The name of the {@link TvInputService} subclass that provides this TV channel. This - * should be a fully qualified class name (such as, "com.example.project.TvInputService"). - * <p> - * This is a required field. - * </p><p> - * Type: TEXT - * </p> - */ - public static final String COLUMN_SERVICE_NAME = "service_name"; - - /** - * The predefined type of this TV channel. - * <p> - * This is primarily used to indicate which broadcast standard (e.g. ATSC, DVB or ISDB) the - * current channel conforms to, with an exception being {@link #TYPE_PASSTHROUGH}, which is - * a special channel type used only by pass-through inputs such as HDMI. The value should - * match to one of the followings: {@link #TYPE_OTHER}, {@link #TYPE_PASSTHROUGH}, - * {@link #TYPE_DVB_T}, {@link #TYPE_DVB_T2}, {@link #TYPE_DVB_S}, {@link #TYPE_DVB_S2}, - * {@link #TYPE_DVB_C}, {@link #TYPE_DVB_C2}, {@link #TYPE_DVB_H}, {@link #TYPE_DVB_SH}, - * {@link #TYPE_ATSC_T}, {@link #TYPE_ATSC_C}, {@link #TYPE_ATSC_M_H}, {@link #TYPE_ISDB_T}, - * {@link #TYPE_ISDB_TB}, {@link #TYPE_ISDB_S}, {@link #TYPE_ISDB_C} {@link #TYPE_1SEG}, - * {@link #TYPE_DTMB}, {@link #TYPE_CMMB}, {@link #TYPE_T_DMB}, {@link #TYPE_S_DMB} - * </p><p> - * This is a required field. - * </p><p> - * Type: INTEGER - * </p> - */ - public static final String COLUMN_TYPE = "type"; - - /** - * The predefined service type of this TV channel. - * <p> - * This is primarily used to indicate whether the current channel is a regular TV channel or - * a radio-like channel. Use the same coding for {@code service_type} in the underlying - * broadcast standard if it is defined there (e.g. ATSC A/53, ETSI EN 300 468 and ARIB - * STD-B10). Otherwise use one of the followings: {@link #SERVICE_TYPE_OTHER}, - * {@link #SERVICE_TYPE_TV}, {@link #SERVICE_TYPE_RADIO} - * </p><p> - * This is a required field. - * </p><p> - * Type: INTEGER - * </p> - */ - public static final String COLUMN_SERVICE_TYPE = "service_type"; - - /** - * The original network ID of this TV channel. - * <p> - * This is used to identify the originating delivery system, if applicable. Use the same - * coding for {@code origianal_network_id} in the underlying broadcast standard if it is - * defined there (e.g. ETSI EN 300 468/TR 101 211 and ARIB STD-B10). If channels cannot be - * globally identified by 2-tuple {{@link #COLUMN_TRANSPORT_STREAM_ID}, - * {@link #COLUMN_SERVICE_ID}}, one must carefully assign a value to this field to form a - * unique 3-tuple identification {{@link #COLUMN_ORIGINAL_NETWORK_ID}, - * {@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}} for its channels. - * </p><p> - * This is a required field if the channel cannot be uniquely identified by a 2-tuple - * {{@link #COLUMN_TRANSPORT_STREAM_ID}, {@link #COLUMN_SERVICE_ID}}. - * </p><p> - * Type: INTEGER - * </p> - */ - public static final String COLUMN_ORIGINAL_NETWORK_ID = "original_network_id"; - - /** - * The transport stream ID of this channel. - * <p> - * This is used to identify the Transport Stream that contains the current channel from any - * other multiplex within a network, if applicable. Use the same coding for - * {@code transport_stream_id} defined in ISO/IEC 13818-1 if the channel is transmitted via - * the MPEG Transport Stream as is the case for many digital broadcast standards. - * </p><p> - * This is a required field if the current channel is transmitted via the MPEG Transport - * Stream. - * </p><p> - * Type: INTEGER - * </p> - */ - public static final String COLUMN_TRANSPORT_STREAM_ID = "transport_stream_id"; - - /** - * The service ID of this channel. - * <p> - * This is used to identify the current service (roughly equivalent to channel) from any - * other service within the Transport Stream, if applicable. Use the same coding for - * {@code service_id} in the underlying broadcast standard if it is defined there (e.g. ETSI - * EN 300 468 and ARIB STD-B10) or {@code program_number} (which usually has the same value - * as {@code service_id}) in ISO/IEC 13818-1 if the channel is transmitted via the MPEG - * Transport Stream. - * </p><p> - * This is a required field if the current channel is transmitted via the MPEG Transport - * Stream. - * </p><p> - * Type: INTEGER - * </p> - */ - public static final String COLUMN_SERVICE_ID = "service_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: TEXT - * </p> - */ - public static final String COLUMN_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 COLUMN_DISPLAY_NAME = "display_name"; - - /** - * The description of this TV channel. - * <p> - * Can be empty initially. - * </p><p> - * Type: TEXT - * </p> - */ - public static final String COLUMN_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 COLUMN_BROWSABLE = "browsable"; - - /** - * Generic data used by individual TV input services. - * <p> - * Type: BLOB - * </p> - */ - public static final String COLUMN_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 COLUMN_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 COLUMN_CHANNEL_ID = "channel_id"; - - /** - * The title of this TV program. - * <p> - * Type: TEXT - * </p> - **/ - public static final String COLUMN_TITLE = "title"; - - /** - * The start time of this TV program, in milliseconds since the epoch. - * <p> - * Type: INTEGER (long) - * </p> - */ - public static final String COLUMN_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 COLUMN_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 COLUMN_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 #COLUMN_DESCRIPTION}. - * </p><p> - * Type: TEXT - * </p> - */ - public static final String COLUMN_LONG_DESCRIPTION = "long_description"; - - /** - * Generic data used by TV input services. - * <p> - * Type: BLOB - * </p> - */ - public static final String COLUMN_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 COLUMN_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 COLUMN_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 COLUMN_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 COLUMN_CHANNEL_ID = "channel_id"; - - /** - * The title of this TV program. - * <p> - * Type: TEXT - * </p> - */ - public static final String COLUMN_TITLE = "title"; - - /** - * The start time of this TV program, in milliseconds since the epoch. - * <p> - * Type: INTEGER (long) - * </p> - */ - public static final String COLUMN_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 COLUMN_END_TIME_UTC_MILLIS = "end_time_utc_millis"; - - /** - * The description of this TV program. - * <p> - * Type: TEXT - * </p> - */ - public static final String COLUMN_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 d4919eb..b3705d8 100644 --- a/core/java/android/service/notification/INotificationListener.aidl +++ b/core/java/android/service/notification/INotificationListener.aidl @@ -17,15 +17,15 @@ package android.service.notification; import android.service.notification.StatusBarNotification; -import android.service.notification.NotificationOrderUpdate; +import android.service.notification.NotificationRankingUpdate; /** @hide */ oneway interface INotificationListener { - void onListenerConnected(in NotificationOrderUpdate update); + void onListenerConnected(in NotificationRankingUpdate update); void onNotificationPosted(in StatusBarNotification notification, - in NotificationOrderUpdate update); + in NotificationRankingUpdate update); void onNotificationRemoved(in StatusBarNotification notification, - in NotificationOrderUpdate update); - void onNotificationOrderUpdate(in NotificationOrderUpdate update); + 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 a94f45a..557f5a6 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -16,21 +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.util.Log; -import java.util.Comparator; -import java.util.HashMap; +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> @@ -49,10 +54,13 @@ public abstract class NotificationListenerService extends Service { + "[" + getClass().getSimpleName() + "]"; private INotificationListenerWrapper mWrapper = null; - private String[] mNotificationKeys; + 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. */ @@ -90,21 +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 order cahnges. - * - * Call {@link #getOrderedNotificationKeys()} to retrieve the new order. + * Implement this method to be notified when the notification ranking changes. + * <P> + * Call {@link #getCurrentRanking()} to retrieve the new ranking. */ - public void onNotificationOrderUpdate() { + public void onNotificationRankingUpdate() { // optional } @@ -218,21 +224,12 @@ public abstract class NotificationListenerService extends Service { * @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, sorted in natural order - * if {@code keys} is {@code null}. - */ - 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); } @@ -240,15 +237,20 @@ public abstract class NotificationListenerService extends Service { } /** - * Request the list of notification keys in their current natural order. - * 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, in their natural order. + * @return A {@link NotificationListenerService.Ranking} object providing + * access to ranking information */ - public String[] getOrderedNotificationKeys() { - return mNotificationKeys; + public Ranking getCurrentRanking() { + return mRanking; } @Override @@ -267,62 +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, - NotificationOrderUpdate update) { - try { - // protect subclass from concurrent modifications of (@link mNotificationKeys}. - synchronized (mWrapper) { - updateNotificationKeys(update); + 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); } - } catch (Throwable t) { - Log.w(TAG, "Error running onOrderedNotificationPosted", t); } } @Override public void onNotificationRemoved(StatusBarNotification sbn, - NotificationOrderUpdate update) { - try { - // protect subclass from concurrent modifications of (@link mNotificationKeys}. - synchronized (mWrapper) { - updateNotificationKeys(update); + 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); } - } catch (Throwable t) { - Log.w(TAG, "Error running onNotificationRemoved", t); } } @Override - public void onListenerConnected(NotificationOrderUpdate update) { - try { - // protect subclass from concurrent modifications of (@link mNotificationKeys}. - synchronized (mWrapper) { - updateNotificationKeys(update); - NotificationListenerService.this.onListenerConnected(mNotificationKeys); + 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); } - } catch (Throwable t) { - Log.w(TAG, "Error running onListenerConnected", t); } } @Override - public void onNotificationOrderUpdate(NotificationOrderUpdate update) + public void onNotificationRankingUpdate(NotificationRankingUpdate update) throws RemoteException { - try { - // protect subclass from concurrent modifications of (@link mNotificationKeys}. - synchronized (mWrapper) { - updateNotificationKeys(update); - NotificationListenerService.this.onNotificationOrderUpdate(); + // 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); } - } catch (Throwable t) { - Log.w(TAG, "Error running onNotificationOrderUpdate", t); } } } - private void updateNotificationKeys(NotificationOrderUpdate update) { - // TODO: avoid garbage by comparing the lists - mNotificationKeys = update.getOrderedKeys(); + 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/NotificationOrderUpdate.java b/core/java/android/service/notification/NotificationOrderUpdate.java deleted file mode 100644 index 20e19a3..0000000 --- a/core/java/android/service/notification/NotificationOrderUpdate.java +++ /dev/null @@ -1,62 +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.service.notification; - -import android.os.Parcel; -import android.os.Parcelable; - -public class NotificationOrderUpdate implements Parcelable { - // TODO replace this with an update instead of the whole array - private final String[] mKeys; - - /** @hide */ - public NotificationOrderUpdate(String[] keys) { - this.mKeys = keys; - } - - public NotificationOrderUpdate(Parcel in) { - this.mKeys = in.readStringArray(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel out, int flags) { - out.writeStringArray(this.mKeys); - } - - public static final Parcelable.Creator<NotificationOrderUpdate> CREATOR - = new Parcelable.Creator<NotificationOrderUpdate>() { - public NotificationOrderUpdate createFromParcel(Parcel parcel) { - return new NotificationOrderUpdate(parcel); - } - - public NotificationOrderUpdate[] newArray(int size) { - return new NotificationOrderUpdate[size]; - } - }; - - /** - * @hide - * @return ordered list of keys - */ - String[] getOrderedKeys() { - return mKeys; - } -} diff --git a/core/java/android/service/notification/NotificationOrderUpdate.aidl b/core/java/android/service/notification/NotificationRankingUpdate.aidl index 5d50641..1393cb9 100644 --- a/core/java/android/service/notification/NotificationOrderUpdate.aidl +++ b/core/java/android/service/notification/NotificationRankingUpdate.aidl @@ -16,4 +16,4 @@ package android.service.notification; -parcelable NotificationOrderUpdate; +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..68a3d30 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -21,6 +21,7 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; +import android.util.Slog; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -37,16 +38,23 @@ import java.util.Objects; * @hide */ public class ZenModeConfig implements Parcelable { + private static String TAG = "ZenModeConfig"; 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"; @@ -59,8 +67,12 @@ public class ZenModeConfig implements Parcelable { private static final String CONDITION_ATT_COMPONENT = "component"; private static final String CONDITION_ATT_ID = "id"; + private static final String EXIT_CONDITION_TAG = "exitCondition"; + private static final String EXIT_CONDITION_ATT_ID = "id"; + public boolean allowCalls; public boolean allowMessages; + public int allowFrom = SOURCE_ANYONE; public String sleepMode; public int sleepStartHour; @@ -69,6 +81,7 @@ public class ZenModeConfig implements Parcelable { public int sleepEndMinute; public ComponentName[] conditionComponents; public Uri[] conditionIds; + public Uri exitConditionId; public ZenModeConfig() { } @@ -92,6 +105,8 @@ public class ZenModeConfig implements Parcelable { conditionIds = new Uri[len]; source.readTypedArray(conditionIds, Uri.CREATOR); } + allowFrom = source.readInt(); + exitConditionId = source.readParcelable(null); } @Override @@ -120,6 +135,8 @@ public class ZenModeConfig implements Parcelable { } else { dest.writeInt(0); } + dest.writeInt(allowFrom); + dest.writeParcelable(exitConditionId, 0); } @Override @@ -127,6 +144,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) @@ -134,9 +152,23 @@ public class ZenModeConfig implements Parcelable { .append(conditionComponents == null ? null : TextUtils.join(",", conditionComponents)) .append(",conditionIds=") .append(conditionIds == null ? null : TextUtils.join(",", conditionIds)) + .append(",exitConditionId=").append(exitConditionId) .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,20 +176,23 @@ 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 && other.sleepEndHour == sleepEndHour && other.sleepEndMinute == sleepEndMinute && Objects.deepEquals(other.conditionComponents, conditionComponents) - && Objects.deepEquals(other.conditionIds, conditionIds); + && Objects.deepEquals(other.conditionIds, conditionIds) + && Objects.equals(other.exitConditionId, exitConditionId); } @Override public int hashCode() { - return Objects.hash(allowCalls, allowMessages, sleepMode, sleepStartHour, - sleepStartMinute, sleepEndHour, sleepEndMinute, - Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds)); + return Objects.hash(allowCalls, allowMessages, allowFrom, sleepMode, + sleepStartHour, sleepStartMinute, sleepEndHour, sleepEndMinute, + Arrays.hashCode(conditionComponents), Arrays.hashCode(conditionIds), + exitConditionId); } public boolean isValid() { @@ -191,6 +226,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) @@ -211,6 +250,8 @@ public class ZenModeConfig implements Parcelable { conditionComponents.add(component); conditionIds.add(conditionId); } + } else if (EXIT_CONDITION_TAG.equals(tag)) { + rt.exitConditionId = safeUri(parser, EXIT_CONDITION_ATT_ID); } } } @@ -224,6 +265,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); @@ -246,6 +288,11 @@ public class ZenModeConfig implements Parcelable { out.endTag(null, CONDITION_TAG); } } + if (exitConditionId != null) { + out.startTag(null, EXIT_CONDITION_TAG); + out.attribute(null, EXIT_CONDITION_ATT_ID, exitConditionId.toString()); + out.endTag(null, EXIT_CONDITION_TAG); + } out.endTag(null, ZEN_TAG); } @@ -309,4 +356,33 @@ public class ZenModeConfig implements Parcelable { return new ZenModeConfig[size]; } }; + + // Built-in countdown conditions, e.g. condition://android/countdown/1399917958951 + + private static final String COUNTDOWN_AUTHORITY = "android"; + private static final String COUNTDOWN_PATH = "countdown"; + + public static Uri toCountdownConditionId(long time) { + return new Uri.Builder().scheme(Condition.SCHEME) + .authority(COUNTDOWN_AUTHORITY) + .appendPath(COUNTDOWN_PATH) + .appendPath(Long.toString(time)) + .build(); + } + + public static long tryParseCountdownConditionId(Uri conditionId) { + if (!Condition.isValidId(conditionId, COUNTDOWN_AUTHORITY)) return 0; + if (conditionId.getPathSegments().size() != 2 + || !COUNTDOWN_PATH.equals(conditionId.getPathSegments().get(0))) return 0; + try { + return Long.parseLong(conditionId.getPathSegments().get(1)); + } catch (RuntimeException e) { + Slog.w(TAG, "Error parsing countdown condition: " + conditionId, e); + return 0; + } + } + + public static boolean isValidCountdownConditionId(Uri conditionId) { + return tryParseCountdownConditionId(conditionId) != 0; + } } 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..a6cddae 100644 --- a/core/java/android/service/trust/TrustAgentService.java +++ b/core/java/android/service/trust/TrustAgentService.java @@ -16,25 +16,32 @@ 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; /** * A service that notifies the system about whether it believes the environment of the device * to be trusted. * + * <p>Trust agents may only be provided by the platform.</p> + * * <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 +54,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 +90,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 +119,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 +127,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 +140,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/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/SidePropagation.java b/core/java/android/transition/SidePropagation.java index 5d38ac8..623cdd1 100644 --- a/core/java/android/transition/SidePropagation.java +++ b/core/java/android/transition/SidePropagation.java @@ -18,6 +18,7 @@ package android.transition; import android.graphics.Rect; import android.util.FloatMath; import android.util.Log; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -32,38 +33,19 @@ import android.view.ViewGroup; public class SidePropagation extends VisibilityPropagation { private static final String TAG = "SlidePropagation"; - /** - * Transition propagates relative to the distance of the left side of the scene. - */ - public static final int LEFT = Slide.LEFT; - - /** - * Transition propagates relative to the distance of the top of the scene. - */ - public static final int TOP = Slide.TOP; - - /** - * Transition propagates relative to the distance of the right side of the scene. - */ - public static final int RIGHT = Slide.RIGHT; - - /** - * Transition propagates relative to the distance of the bottom of the scene. - */ - public static final int BOTTOM = Slide.BOTTOM; - private float mPropagationSpeed = 3.0f; - private int mSide = BOTTOM; + private int mSide = Gravity.BOTTOM; /** * Sets the side that is used to calculate the transition propagation. If the transitioning * View is visible in the start of the transition, then it will transition sooner when * closer to the side and later when farther. If the view is not visible in the start of * the transition, then it will transition later when closer to the side and sooner when - * farther from the edge. The default is {@link #BOTTOM}. + * farther from the edge. The default is {@link Gravity#BOTTOM}. * * @param side The side that is used to calculate the transition propagation. Must be one of - * {@link #LEFT}, {@link #TOP}, {@link #RIGHT}, or {@link #BOTTOM}. + * {@link Gravity#LEFT}, {@link Gravity#TOP}, {@link Gravity#RIGHT}, or + * {@link Gravity#BOTTOM}. */ public void setSide(int side) { mSide = side; @@ -141,16 +123,16 @@ public class SidePropagation extends VisibilityPropagation { int left, int top, int right, int bottom) { int distance = 0; switch (mSide) { - case LEFT: + case Gravity.LEFT: distance = right - viewX + Math.abs(epicenterY - viewY); break; - case TOP: + case Gravity.TOP: distance = bottom - viewY + Math.abs(epicenterX - viewX); break; - case RIGHT: + case Gravity.RIGHT: distance = viewX - left + Math.abs(epicenterY - viewY); break; - case BOTTOM: + case Gravity.BOTTOM: distance = viewY - top + Math.abs(epicenterX - viewX); break; } @@ -159,8 +141,8 @@ public class SidePropagation extends VisibilityPropagation { private int getMaxDistance(ViewGroup sceneRoot) { switch (mSide) { - case LEFT: - case RIGHT: + case Gravity.LEFT: + case Gravity.RIGHT: return sceneRoot.getWidth(); default: return sceneRoot.getHeight(); diff --git a/core/java/android/transition/Slide.java b/core/java/android/transition/Slide.java index 0ff8ddd..8269258 100644 --- a/core/java/android/transition/Slide.java +++ b/core/java/android/transition/Slide.java @@ -24,6 +24,7 @@ import android.animation.ValueAnimator; import android.graphics.Rect; import android.util.Log; import android.util.Property; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.animation.AccelerateInterpolator; @@ -41,35 +42,9 @@ import android.view.animation.DecelerateInterpolator; public class Slide extends Visibility { private static final String TAG = "Slide"; - /** - * Move Views in or out of the left edge of the scene. - * @see #setSlideEdge(int) - */ - public static final int LEFT = 0; - - /** - * Move Views in or out of the top edge of the scene. - * @see #setSlideEdge(int) - */ - public static final int TOP = 1; - - /** - * Move Views in or out of the right edge of the scene. - * @see #setSlideEdge(int) - */ - public static final int RIGHT = 2; - - /** - * Move Views in or out of the bottom edge of the scene. This is the - * default slide direction. - * @see #setSlideEdge(int) - */ - public static final int BOTTOM = 3; - private static final TimeInterpolator sDecelerate = new DecelerateInterpolator(); private static final TimeInterpolator sAccelerate = new AccelerateInterpolator(); - private int[] mTempLoc = new int[2]; private CalculateSlide mSlideCalculator = sCalculateBottom; private interface CalculateSlide { @@ -136,11 +111,11 @@ public class Slide extends Visibility { }; /** - * Constructor using the default {@link android.transition.Slide#BOTTOM} + * Constructor using the default {@link Gravity#BOTTOM} * slide edge direction. */ public Slide() { - setSlideEdge(BOTTOM); + setSlideEdge(Gravity.BOTTOM); } /** @@ -152,20 +127,22 @@ public class Slide extends Visibility { /** * Change the edge that Views appear and disappear from. - * @param slideEdge The edge of the scene to use for Views appearing and disappearing. + * @param slideEdge The edge of the scene to use for Views appearing and disappearing. One of + * {@link android.view.Gravity#LEFT}, {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#RIGHT}, {@link android.view.Gravity#BOTTOM}. */ public void setSlideEdge(int slideEdge) { switch (slideEdge) { - case LEFT: + case Gravity.LEFT: mSlideCalculator = sCalculateLeft; break; - case TOP: + case Gravity.TOP: mSlideCalculator = sCalculateTop; break; - case RIGHT: + case Gravity.RIGHT: mSlideCalculator = sCalculateRight; break; - case BOTTOM: + case Gravity.BOTTOM: mSlideCalculator = sCalculateBottom; break; default: diff --git a/core/java/android/transition/Transition.java b/core/java/android/transition/Transition.java index 5a432dc..e9c2bba 100644 --- a/core/java/android/transition/Transition.java +++ b/core/java/android/transition/Transition.java @@ -24,7 +24,6 @@ import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; import android.util.SparseArray; -import android.util.SparseIntArray; import android.util.SparseLongArray; import android.view.SurfaceView; import android.view.TextureView; @@ -108,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; @@ -127,6 +160,7 @@ public abstract class Transition implements Cloneable { 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 = @@ -338,6 +372,53 @@ 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. */ @@ -464,6 +545,37 @@ public abstract class Transition implements Cloneable { } } + 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 @@ -480,21 +592,9 @@ public abstract class Transition implements Cloneable { if (DBG) { Log.d(LOG_TAG, "createAnimators() for " + this); } - ArrayMap<View, TransitionValues> unmatchedStart = - new ArrayMap<View, TransitionValues>(startValues.viewValues); - ArrayMap<View, TransitionValues> unmatchedEnd = - new ArrayMap<View, TransitionValues>(endValues.viewValues); - ArrayList<TransitionValues> startValuesList = new ArrayList<TransitionValues>(); ArrayList<TransitionValues> endValuesList = new ArrayList<TransitionValues>(); - matchNames(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, - startValues.nameValues, endValues.nameValues); - matchInstances(startValuesList, endValuesList, unmatchedStart, unmatchedEnd); - matchIds(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, - startValues.idValues, endValues.idValues); - matchItemIds(startValuesList, endValuesList, unmatchedStart, unmatchedEnd, - startValues.itemIdValues, endValues.itemIdValues); - addUnmatched(startValuesList, endValuesList, unmatchedStart, unmatchedEnd); + matchStartAndEnd(startValues, endValues, startValuesList, endValuesList); ArrayMap<Animator, AnimationInfo> runningAnimators = getRunningAnimators(); long minStartDelay = Long.MAX_VALUE; @@ -1663,7 +1763,7 @@ public abstract class Transition implements Cloneable { /** * Sets the callback to use to find the epicenter of a Transition. A null value indicates - * that there is no epicenter in the Transition and getEpicenter() will return null. + * that there is no epicenter in the Transition and onGetEpicenter() will return null. * Transitions like {@link android.transition.Explode} use a point or Rect to orient * the direction of travel. This is called the epicenter of the Transition and is * typically centered on a touched View. The @@ -1699,7 +1799,7 @@ public abstract class Transition implements Cloneable { if (mEpicenterCallback == null) { return null; } - return mEpicenterCallback.getEpicenter(this); + return mEpicenterCallback.onGetEpicenter(this); } /** @@ -2012,6 +2112,6 @@ public abstract class Transition implements Cloneable { * @return The Rect region of the epicenter of <code>transition</code> or null if * there is no epicenter. */ - public abstract Rect getEpicenter(Transition transition); + public abstract Rect onGetEpicenter(Transition transition); } } diff --git a/core/java/android/transition/TransitionInflater.java b/core/java/android/transition/TransitionInflater.java index 04f8672..5b7c737 100644 --- a/core/java/android/transition/TransitionInflater.java +++ b/core/java/android/transition/TransitionInflater.java @@ -22,6 +22,7 @@ import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.util.AttributeSet; import android.util.Xml; +import android.view.Gravity; import android.view.InflateException; import android.view.ViewGroup; import android.view.animation.AnimationUtils; @@ -30,6 +31,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 +42,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; @@ -203,7 +209,7 @@ public class TransitionInflater { private Slide createSlideTransition(AttributeSet attrs) { TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.Slide); - int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Slide.BOTTOM); + int edge = a.getInt(com.android.internal.R.styleable.Slide_slideEdge, Gravity.BOTTOM); Slide slide = new Slide(edge); a.recycle(); return slide; @@ -266,6 +272,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 { @@ -284,6 +317,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/tv/ITvInputManager.aidl b/core/java/android/tv/ITvInputManager.aidl deleted file mode 100644 index b756aba..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 String inputId, int userId); - - void registerCallback(in ITvInputClient client, in String inputId, int userId); - void unregisterCallback(in ITvInputClient client, in String inputId, int userId); - - void createSession(in ITvInputClient client, in String inputId, 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 50462cc..0000000 --- a/core/java/android/tv/TvInputInfo.java +++ /dev/null @@ -1,163 +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 = generateInputIdForComponenetName(new ComponentName(si.packageName, si.name)); - } - - /** - * 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 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 generate an input id from a ComponentName. - * - * @param name the component name for generating an input id. - * @return the generated input id for the given {@code name}. - * @hide - */ - public static final String generateInputIdForComponenetName(ComponentName name) { - return name.flattenToShortString(); - } - - /** - * 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 c5f179a..0000000 --- a/core/java/android/tv/TvInputManager.java +++ /dev/null @@ -1,783 +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.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<String, List<TvInputListenerRecord>> mTvInputListenerRecordsMap = - new HashMap<String, List<TvInputListenerRecord>>(); - - // A mapping from the sequence number of a session to its SessionCallbackRecord. - private final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap = - new SparseArray<SessionCallbackRecord>(); - - // A sequence number for the next session to be created. Should be protected by a lock - // {@code mSessionCallbackRecordMap}. - private int mNextSeq; - - private final ITvInputClient mClient; - - private final int mUserId; - - /** - * Interface used to receive the created session. - */ - public abstract static class SessionCallback { - /** - * 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. - */ - public void onSessionCreated(Session session) { - } - - /** - * This is called when {@link TvInputManager.Session} is released. - * This typically happens when the process hosting the session has crashed or been killed. - * - * @param session A {@link TvInputManager.Session} instance released. - */ - public void onSessionReleased(Session session) { - } - } - - private static final class SessionCallbackRecord { - private final SessionCallback mSessionCallback; - private final Handler mHandler; - private Session mSession; - - public SessionCallbackRecord(SessionCallback sessionCallback, - Handler handler) { - mSessionCallback = sessionCallback; - mHandler = handler; - } - - public void postSessionCreated(final Session session) { - mSession = session; - mHandler.post(new Runnable() { - @Override - public void run() { - mSessionCallback.onSessionCreated(session); - } - }); - } - - public void postSessionReleased() { - mHandler.post(new Runnable() { - @Override - public void run() { - mSessionCallback.onSessionReleased(mSession); - } - }); - } - } - - /** - * 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 inputId the id of the TV input. - * @param isAvailable {@code true} if the given TV input is available to show TV programs. - * {@code false} otherwise. - */ - public void onAvailabilityChanged(String inputId, 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 String inputId, final boolean isAvailable) { - mHandler.post(new Runnable() { - @Override - public void run() { - mListener.onAvailabilityChanged(inputId, isAvailable); - } - }); - } - } - - /** - * @hide - */ - public TvInputManager(ITvInputManager service, int userId) { - mService = service; - mUserId = userId; - mClient = new ITvInputClient.Stub() { - @Override - public void onSessionCreated(String inputId, IBinder token, InputChannel channel, - int seq) { - synchronized (mSessionCallbackRecordMap) { - SessionCallbackRecord record = mSessionCallbackRecordMap.get(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, seq, - mSessionCallbackRecordMap); - } - record.postSessionCreated(session); - } - } - - @Override - public void onSessionReleased(int seq) { - synchronized (mSessionCallbackRecordMap) { - SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); - mSessionCallbackRecordMap.delete(seq); - if (record == null) { - Log.e(TAG, "Callback not found for seq:" + seq); - return; - } - record.mSession.releaseInternal(); - record.postSessionReleased(); - } - } - - @Override - public void onAvailabilityChanged(String inputId, boolean isAvailable) { - synchronized (mTvInputListenerRecordsMap) { - List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); - 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(inputId, 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 inputId the id of the 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(String inputId) { - if (inputId == null) { - throw new IllegalArgumentException("id cannot be null"); - } - synchronized (mTvInputListenerRecordsMap) { - List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); - if (records == null || records.size() == 0) { - throw new IllegalStateException("At least one listener should be registered."); - } - } - try { - return mService.getAvailability(mClient, inputId, mUserId); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - - /** - * Registers a {@link TvInputListener} for a given TV input. - * - * @param inputId the id of the 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(String inputId, TvInputListener listener, Handler handler) { - if (inputId == null) { - throw new IllegalArgumentException("id 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(inputId); - if (records == null) { - records = new ArrayList<TvInputListenerRecord>(); - mTvInputListenerRecordsMap.put(inputId, records); - try { - mService.registerCallback(mClient, inputId, 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 inputId the id of the 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(String inputId, final TvInputListener listener) { - if (inputId == null) { - throw new IllegalArgumentException("id cannot be null"); - } - if (listener == null) { - throw new IllegalArgumentException("listener cannot be null"); - } - synchronized (mTvInputListenerRecordsMap) { - List<TvInputListenerRecord> records = mTvInputListenerRecordsMap.get(inputId); - if (records == null) { - Log.e(TAG, "No listener found for " + inputId); - 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, inputId, mUserId); - } catch (RemoteException e) { - throw new RuntimeException(e); - } finally { - mTvInputListenerRecordsMap.remove(inputId); - } - } - } - } - - /** - * 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 inputId the id of the 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(String inputId, final SessionCallback callback, - Handler handler) { - if (inputId == null) { - throw new IllegalArgumentException("id cannot be null"); - } - if (callback == null) { - throw new IllegalArgumentException("callback cannot be null"); - } - if (handler == null) { - throw new IllegalArgumentException("handler cannot be null"); - } - SessionCallbackRecord record = new SessionCallbackRecord(callback, handler); - synchronized (mSessionCallbackRecordMap) { - int seq = mNextSeq++; - mSessionCallbackRecordMap.put(seq, record); - try { - mService.createSession(mClient, inputId, 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; - private final int mSeq; - - // 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 final SparseArray<SessionCallbackRecord> mSessionCallbackRecordMap; - - private IBinder mToken; - private TvInputEventSender mSender; - private InputChannel mChannel; - - /** @hide */ - private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId, - int seq, SparseArray<SessionCallbackRecord> sessionCallbackRecordMap) { - mToken = token; - mChannel = channel; - mService = service; - mUserId = userId; - mSeq = seq; - mSessionCallbackRecordMap = sessionCallbackRecordMap; - } - - /** - * 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); - } catch (RemoteException e) { - throw new RuntimeException(e); - } - - releaseInternal(); - } - - /** - * 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 void releaseInternal() { - mToken = null; - synchronized (mHandler) { - if (mChannel != null) { - if (mSender != null) { - flushPendingEventsLocked(); - mSender.dispose(); - mSender = null; - } - mChannel.dispose(); - mChannel = null; - } - } - synchronized (mSessionCallbackRecordMap) { - mSessionCallbackRecordMap.remove(mSeq); - } - } - - 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 eeb738d..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 String mId; - private final Handler mHandler = new ServiceHandler(); - private final RemoteCallbackList<ITvInputServiceCallback> mCallbacks = - new RemoteCallbackList<ITvInputServiceCallback>(); - private boolean mAvailable; - - @Override - public void onCreate() { - super.onCreate(); - mId = TvInputInfo.generateInputIdForComponenetName( - 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(mId, 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 || !mOverlayView.isAttachedToWindow()) { - 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(mId, 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 7721575..0000000 --- a/core/java/android/tv/TvView.java +++ /dev/null @@ -1,382 +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.os.Handler; -import android.text.TextUtils; -import android.tv.TvInputManager.Session; -import android.tv.TvInputManager.Session.FinishedInputEventCallback; -import android.tv.TvInputManager.SessionCallback; -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; -import android.view.ViewRootImpl; - -/** - * 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 SessionCallback mSessionCallback; - 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; - } - ViewRootImpl viewRootImpl = getViewRootImpl(); - if (viewRootImpl != null) { - viewRootImpl.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 SessionCallback#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 inputId the id of TV input which 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(String inputId, SessionCallback callback) { - if (TextUtils.isEmpty(inputId)) { - throw new IllegalArgumentException("inputId cannot be null or an empty string"); - } - 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, mSessionCallback - // is newly assigned for every bindTvInput call and compared with - // MySessionCreateCallback.this. - mSessionCallback = new MySessionCallback(callback); - mTvInputManager.createSession(inputId, mSessionCallback, 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; - } - InputEvent copiedEvent = event.copy(); - int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, 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; - } - InputEvent copiedEvent = event.copy(); - int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, 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; - } - InputEvent copiedEvent = event.copy(); - int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, 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; - } - InputEvent copiedEvent = event.copy(); - int ret = mSession.dispatchInputEvent(copiedEvent, copiedEvent, 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 MySessionCallback extends SessionCallback { - final SessionCallback mExternalCallback; - - MySessionCallback(SessionCallback externalCallback) { - mExternalCallback = externalCallback; - } - - @Override - public void onSessionCreated(Session session) { - if (this != mSessionCallback) { - // This callback is obsolete. - if (session != null) { - 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); - } - } - - @Override - public void onSessionReleased(Session session) { - mSession = null; - if (mExternalCallback != null) { - mExternalCallback.onSessionReleased(session); - } - } - } -} diff --git a/core/java/android/hardware/camera2/Rational.java b/core/java/android/util/Rational.java index 693ee2b..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 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..b5b9199 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); @@ -277,32 +217,6 @@ final class HardwareLayer { private static native void nUpdateRenderLayer(long layerUpdater, long displayList, int left, int top, int right, int bottom); - private static native boolean nFlushChanges(long layerUpdater); - private static native long nGetLayer(long layerUpdater); private static native int nGetTexName(long layerUpdater); - - private static class Finalizer { - private long mDeferredUpdater; - - public Finalizer(long deferredUpdater) { - mDeferredUpdater = deferredUpdater; - } - - @Override - protected void finalize() throws Throwable { - try { - destroy(); - } finally { - super.finalize(); - } - } - - void destroy() { - if (mDeferredUpdater != 0) { - nDestroyLayerUpdater(mDeferredUpdater); - mDeferredUpdater = 0; - } - } - } } diff --git a/core/java/android/view/HardwareRenderer.java b/core/java/android/view/HardwareRenderer.java index e366697..592dec8 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. */ @@ -389,8 +363,7 @@ public abstract class HardwareRenderer { * @param callbacks Callbacks invoked when drawing happens. * @param dirty The dirty rectangle to update, can be null. */ - abstract void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks, - Rect dirty); + abstract void draw(View view, View.AttachInfo attachInfo, HardwareDrawCallbacks callbacks); /** * Creates a new hardware layer. A hardware layer built by calling this @@ -443,17 +416,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 +435,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 +465,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 +492,7 @@ public abstract class HardwareRenderer { * see {@link android.content.ComponentCallbacks} */ static void startTrimMemory(int level) { - GLRenderer.startTrimMemory(level); + ThreadedRenderer.startTrimMemory(level); } /** @@ -522,7 +500,7 @@ public abstract class HardwareRenderer { * cleanup special resources used by the memory trimming process. */ static void endTrimMemory() { - GLRenderer.endTrimMemory(); + ThreadedRenderer.endTrimMemory(); } /** @@ -569,96 +547,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..ae59bbc 100644 --- a/core/java/android/view/IWindowManager.aidl +++ b/core/java/android/view/IWindowManager.aidl @@ -30,6 +30,7 @@ import android.view.IApplicationToken; import android.view.IOnKeyguardExitResult; import android.view.IRotationWatcher; import android.view.IWindowSession; +import android.view.IWindowSessionCallback; import android.view.KeyEvent; import android.view.InputEvent; import android.view.MagnificationSpec; @@ -56,7 +57,7 @@ interface IWindowManager boolean stopViewServer(); // Transaction #2 boolean isViewServerRunning(); // Transaction #3 - IWindowSession openSession(in IInputMethodClient client, + IWindowSession openSession(in IWindowSessionCallback callback, in IInputMethodClient client, in IInputContext inputContext); boolean inputMethodClientHasFocus(IInputMethodClient client); @@ -79,7 +80,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 +121,7 @@ interface IWindowManager boolean isKeyguardSecure(); boolean inKeyguardRestrictedInputMode(); void dismissKeyguard(); + void keyguardGoingAway(); void closeSystemDialogs(String reason); @@ -129,6 +131,8 @@ interface IWindowManager void setAnimationScale(int which, float scale); void setAnimationScales(in float[] scales); + float getCurrentAnimatorScale(); + // For testing void setInTouchMode(boolean showFocus); @@ -215,12 +219,6 @@ interface IWindowManager oneway void statusBarVisibilityChanged(int visibility); /** - * Block until the given window has been drawn to the screen. - * Returns true if really waiting, false if the window does not exist. - */ - boolean waitForWindowDrawn(IBinder token, in IRemoteCallback callback); - - /** * Device has a software navigation bar (separate from the status bar). */ boolean hasNavigationBar(); diff --git a/core/java/android/view/IWindowSessionCallback.aidl b/core/java/android/view/IWindowSessionCallback.aidl new file mode 100644 index 0000000..88931ce --- /dev/null +++ b/core/java/android/view/IWindowSessionCallback.aidl @@ -0,0 +1,27 @@ +/* +** 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.view; + +/** + * Callback to active sessions of the window manager + * + * {@hide} + */ +oneway interface IWindowSessionCallback +{ + void onAnimatorScaleChanged(float scale); +} 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 852fce5..8b2ec7a 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 @@ -1699,6 +1716,7 @@ public class KeyEvent extends InputEvent implements Parcelable { case KeyEvent.KEYCODE_MENU: case KeyEvent.KEYCODE_SLEEP: case KeyEvent.KEYCODE_WAKEUP: + case KeyEvent.KEYCODE_PAIRING: return true; } return false; diff --git a/core/java/android/view/RenderNode.java b/core/java/android/view/RenderNode.java index 0cfde94..4631b64 100644 --- a/core/java/android/view/RenderNode.java +++ b/core/java/android/view/RenderNode.java @@ -325,8 +325,8 @@ public class RenderNode { * * @hide */ - public void setCaching(boolean caching) { - nSetCaching(mNativeRenderNode, caching); + public boolean setCaching(boolean caching) { + return nSetCaching(mNativeRenderNode, caching); } /** @@ -335,8 +335,8 @@ public class RenderNode { * * @param clipToBounds true if the display list should clip to its bounds */ - public void setClipToBounds(boolean clipToBounds) { - nSetClipToBounds(mNativeRenderNode, clipToBounds); + public boolean setClipToBounds(boolean clipToBounds) { + return nSetClipToBounds(mNativeRenderNode, clipToBounds); } /** @@ -346,8 +346,8 @@ public class RenderNode { * @param shouldProject true if the display list should be projected onto a * containing volume. */ - public void setProjectBackwards(boolean shouldProject) { - nSetProjectBackwards(mNativeRenderNode, shouldProject); + public boolean setProjectBackwards(boolean shouldProject) { + return nSetProjectBackwards(mNativeRenderNode, shouldProject); } /** @@ -355,8 +355,8 @@ public class RenderNode { * DisplayList should draw any descendent DisplayLists with * ProjectBackwards=true directly on top of it. Default value is false. */ - public void setProjectionReceiver(boolean shouldRecieve) { - nSetProjectionReceiver(mNativeRenderNode, shouldRecieve); + public boolean setProjectionReceiver(boolean shouldRecieve) { + return nSetProjectionReceiver(mNativeRenderNode, shouldRecieve); } /** @@ -365,17 +365,16 @@ public class RenderNode { * * Deep copies the data into native to simplify reference ownership. */ - public void setOutline(Outline outline) { - if (outline == null) { - nSetOutlineEmpty(mNativeRenderNode); - } else if (!outline.isValid()) { - throw new IllegalArgumentException("Outline must be valid"); + public boolean setOutline(Outline outline) { + if (outline == null || outline.isEmpty()) { + return nSetOutlineEmpty(mNativeRenderNode); } else if (outline.mRect != null) { - nSetOutlineRoundRect(mNativeRenderNode, outline.mRect.left, outline.mRect.top, + return nSetOutlineRoundRect(mNativeRenderNode, outline.mRect.left, outline.mRect.top, outline.mRect.right, outline.mRect.bottom, outline.mRadius); } else if (outline.mPath != null) { - nSetOutlineConvexPath(mNativeRenderNode, outline.mPath.mNativePath); + return nSetOutlineConvexPath(mNativeRenderNode, outline.mPath.mNativePath); } + throw new IllegalArgumentException("Unrecognized outline?"); } /** @@ -383,16 +382,20 @@ public class RenderNode { * * @param clipToOutline true if clipping to the outline. */ - public void setClipToOutline(boolean clipToOutline) { - nSetClipToOutline(mNativeRenderNode, clipToOutline); + public boolean setClipToOutline(boolean clipToOutline) { + return nSetClipToOutline(mNativeRenderNode, clipToOutline); + } + + public boolean getClipToOutline() { + return nGetClipToOutline(mNativeRenderNode); } /** * Controls the RenderNode's circular reveal clip. */ - public void setRevealClip(boolean shouldClip, boolean inverseClip, + public boolean setRevealClip(boolean shouldClip, boolean inverseClip, float x, float y, float radius) { - nSetRevealClip(mNativeRenderNode, shouldClip, inverseClip, x, y, radius); + return nSetRevealClip(mNativeRenderNode, shouldClip, inverseClip, x, y, radius); } /** @@ -401,8 +404,8 @@ public class RenderNode { * * @param matrix A transform matrix to apply to this display list */ - public void setStaticMatrix(Matrix matrix) { - nSetStaticMatrix(mNativeRenderNode, matrix.native_instance); + public boolean setStaticMatrix(Matrix matrix) { + return nSetStaticMatrix(mNativeRenderNode, matrix.native_instance); } /** @@ -415,8 +418,8 @@ public class RenderNode { * * @hide */ - public void setAnimationMatrix(Matrix matrix) { - nSetAnimationMatrix(mNativeRenderNode, + public boolean setAnimationMatrix(Matrix matrix) { + return nSetAnimationMatrix(mNativeRenderNode, (matrix != null) ? matrix.native_instance : 0); } @@ -428,8 +431,8 @@ public class RenderNode { * @see View#setAlpha(float) * @see #getAlpha() */ - public void setAlpha(float alpha) { - nSetAlpha(mNativeRenderNode, alpha); + public boolean setAlpha(float alpha) { + return nSetAlpha(mNativeRenderNode, alpha); } /** @@ -454,8 +457,8 @@ public class RenderNode { * @see android.view.View#hasOverlappingRendering() * @see #hasOverlappingRendering() */ - public void setHasOverlappingRendering(boolean hasOverlappingRendering) { - nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); + public boolean setHasOverlappingRendering(boolean hasOverlappingRendering) { + return nSetHasOverlappingRendering(mNativeRenderNode, hasOverlappingRendering); } /** @@ -470,8 +473,8 @@ public class RenderNode { return nHasOverlappingRendering(mNativeRenderNode); } - public void setElevation(float lift) { - nSetElevation(mNativeRenderNode, lift); + public boolean setElevation(float lift) { + return nSetElevation(mNativeRenderNode, lift); } public float getElevation() { @@ -486,8 +489,8 @@ public class RenderNode { * @see View#setTranslationX(float) * @see #getTranslationX() */ - public void setTranslationX(float translationX) { - nSetTranslationX(mNativeRenderNode, translationX); + public boolean setTranslationX(float translationX) { + return nSetTranslationX(mNativeRenderNode, translationX); } /** @@ -507,8 +510,8 @@ public class RenderNode { * @see View#setTranslationY(float) * @see #getTranslationY() */ - public void setTranslationY(float translationY) { - nSetTranslationY(mNativeRenderNode, translationY); + public boolean setTranslationY(float translationY) { + return nSetTranslationY(mNativeRenderNode, translationY); } /** @@ -526,8 +529,8 @@ public class RenderNode { * @see View#setTranslationZ(float) * @see #getTranslationZ() */ - public void setTranslationZ(float translationZ) { - nSetTranslationZ(mNativeRenderNode, translationZ); + public boolean setTranslationZ(float translationZ) { + return nSetTranslationZ(mNativeRenderNode, translationZ); } /** @@ -547,8 +550,8 @@ public class RenderNode { * @see View#setRotation(float) * @see #getRotation() */ - public void setRotation(float rotation) { - nSetRotation(mNativeRenderNode, rotation); + public boolean setRotation(float rotation) { + return nSetRotation(mNativeRenderNode, rotation); } /** @@ -568,8 +571,8 @@ public class RenderNode { * @see View#setRotationX(float) * @see #getRotationX() */ - public void setRotationX(float rotationX) { - nSetRotationX(mNativeRenderNode, rotationX); + public boolean setRotationX(float rotationX) { + return nSetRotationX(mNativeRenderNode, rotationX); } /** @@ -589,8 +592,8 @@ public class RenderNode { * @see View#setRotationY(float) * @see #getRotationY() */ - public void setRotationY(float rotationY) { - nSetRotationY(mNativeRenderNode, rotationY); + public boolean setRotationY(float rotationY) { + return nSetRotationY(mNativeRenderNode, rotationY); } /** @@ -610,8 +613,8 @@ public class RenderNode { * @see View#setScaleX(float) * @see #getScaleX() */ - public void setScaleX(float scaleX) { - nSetScaleX(mNativeRenderNode, scaleX); + public boolean setScaleX(float scaleX) { + return nSetScaleX(mNativeRenderNode, scaleX); } /** @@ -631,8 +634,8 @@ public class RenderNode { * @see View#setScaleY(float) * @see #getScaleY() */ - public void setScaleY(float scaleY) { - nSetScaleY(mNativeRenderNode, scaleY); + public boolean setScaleY(float scaleY) { + return nSetScaleY(mNativeRenderNode, scaleY); } /** @@ -652,8 +655,8 @@ public class RenderNode { * @see View#setPivotX(float) * @see #getPivotX() */ - public void setPivotX(float pivotX) { - nSetPivotX(mNativeRenderNode, pivotX); + public boolean setPivotX(float pivotX) { + return nSetPivotX(mNativeRenderNode, pivotX); } /** @@ -673,8 +676,8 @@ public class RenderNode { * @see View#setPivotY(float) * @see #getPivotY() */ - public void setPivotY(float pivotY) { - nSetPivotY(mNativeRenderNode, pivotY); + public boolean setPivotY(float pivotY) { + return nSetPivotY(mNativeRenderNode, pivotY); } /** @@ -700,8 +703,8 @@ public class RenderNode { * @see View#setCameraDistance(float) * @see #getCameraDistance() */ - public void setCameraDistance(float distance) { - nSetCameraDistance(mNativeRenderNode, distance); + public boolean setCameraDistance(float distance) { + return nSetCameraDistance(mNativeRenderNode, distance); } /** @@ -721,8 +724,8 @@ public class RenderNode { * @see View#setLeft(int) * @see #getLeft() */ - public void setLeft(int left) { - nSetLeft(mNativeRenderNode, left); + public boolean setLeft(int left) { + return nSetLeft(mNativeRenderNode, left); } /** @@ -742,8 +745,8 @@ public class RenderNode { * @see View#setTop(int) * @see #getTop() */ - public void setTop(int top) { - nSetTop(mNativeRenderNode, top); + public boolean setTop(int top) { + return nSetTop(mNativeRenderNode, top); } /** @@ -763,8 +766,8 @@ public class RenderNode { * @see View#setRight(int) * @see #getRight() */ - public void setRight(int right) { - nSetRight(mNativeRenderNode, right); + public boolean setRight(int right) { + return nSetRight(mNativeRenderNode, right); } /** @@ -784,8 +787,8 @@ public class RenderNode { * @see View#setBottom(int) * @see #getBottom() */ - public void setBottom(int bottom) { - nSetBottom(mNativeRenderNode, bottom); + public boolean setBottom(int bottom) { + return nSetBottom(mNativeRenderNode, bottom); } /** @@ -810,8 +813,8 @@ public class RenderNode { * @see View#setRight(int) * @see View#setBottom(int) */ - public void setLeftTopRightBottom(int left, int top, int right, int bottom) { - nSetLeftTopRightBottom(mNativeRenderNode, left, top, right, bottom); + public boolean setLeftTopRightBottom(int left, int top, int right, int bottom) { + return nSetLeftTopRightBottom(mNativeRenderNode, left, top, right, bottom); } /** @@ -822,8 +825,8 @@ public class RenderNode { * * @see View#offsetLeftAndRight(int) */ - public void offsetLeftAndRight(float offset) { - nOffsetLeftAndRight(mNativeRenderNode, offset); + public boolean offsetLeftAndRight(float offset) { + return nOffsetLeftAndRight(mNativeRenderNode, offset); } /** @@ -834,8 +837,15 @@ public class RenderNode { * * @see View#offsetTopAndBottom(int) */ - public void offsetTopAndBottom(float offset) { - nOffsetTopAndBottom(mNativeRenderNode, offset); + public boolean offsetTopAndBottom(float offset) { + return nOffsetTopAndBottom(mNativeRenderNode, offset); + } + + /** + * Sets the scroll position, this is used for damage calculations + */ + public void setScrollPosition(int x, int y) { + nSetScrollPosition(mNativeRenderNode, x, y); } /** @@ -848,6 +858,13 @@ public class RenderNode { nOutput(mNativeRenderNode); } + /** + * Gets the size of the DisplayList for debug purposes. + */ + public int getDebugSize() { + return nGetDebugSize(mNativeRenderNode); + } + /////////////////////////////////////////////////////////////////////////// // Animations /////////////////////////////////////////////////////////////////////////// @@ -881,44 +898,46 @@ public class RenderNode { // Properties - private static native void nOffsetTopAndBottom(long renderNode, float offset); - private static native void nOffsetLeftAndRight(long renderNode, float offset); - private static native void nSetLeftTopRightBottom(long renderNode, int left, int top, + private static native boolean nOffsetTopAndBottom(long renderNode, float offset); + private static native boolean nOffsetLeftAndRight(long renderNode, float offset); + private static native boolean nSetLeftTopRightBottom(long renderNode, int left, int top, int right, int bottom); - private static native void nSetBottom(long renderNode, int bottom); - private static native void nSetRight(long renderNode, int right); - private static native void nSetTop(long renderNode, int top); - private static native void nSetLeft(long renderNode, int left); - private static native void nSetCameraDistance(long renderNode, float distance); - private static native void nSetPivotY(long renderNode, float pivotY); - private static native void nSetPivotX(long renderNode, float pivotX); - private static native void nSetCaching(long renderNode, boolean caching); - private static native void nSetClipToBounds(long renderNode, boolean clipToBounds); - private static native void nSetProjectBackwards(long renderNode, boolean shouldProject); - private static native void nSetProjectionReceiver(long renderNode, boolean shouldRecieve); - private static native void nSetOutlineRoundRect(long renderNode, int left, int top, + private static native boolean nSetBottom(long renderNode, int bottom); + private static native boolean nSetRight(long renderNode, int right); + private static native boolean nSetTop(long renderNode, int top); + private static native boolean nSetLeft(long renderNode, int left); + private static native void nSetScrollPosition(long renderNode, int scrollX, int scrollY); + private static native boolean nSetCameraDistance(long renderNode, float distance); + private static native boolean nSetPivotY(long renderNode, float pivotY); + private static native boolean nSetPivotX(long renderNode, float pivotX); + private static native boolean nSetCaching(long renderNode, boolean caching); + private static native boolean nSetClipToBounds(long renderNode, boolean clipToBounds); + private static native boolean nSetProjectBackwards(long renderNode, boolean shouldProject); + private static native boolean nSetProjectionReceiver(long renderNode, boolean shouldRecieve); + private static native boolean nSetOutlineRoundRect(long renderNode, int left, int top, int right, int bottom, float radius); - private static native void nSetOutlineConvexPath(long renderNode, long nativePath); - private static native void nSetOutlineEmpty(long renderNode); - private static native void nSetClipToOutline(long renderNode, boolean clipToOutline); - private static native void nSetRevealClip(long renderNode, + private static native boolean nSetOutlineConvexPath(long renderNode, long nativePath); + private static native boolean nSetOutlineEmpty(long renderNode); + private static native boolean nSetClipToOutline(long renderNode, boolean clipToOutline); + private static native boolean nSetRevealClip(long renderNode, boolean shouldClip, boolean inverseClip, float x, float y, float radius); - private static native void nSetAlpha(long renderNode, float alpha); - private static native void nSetHasOverlappingRendering(long renderNode, + private static native boolean nSetAlpha(long renderNode, float alpha); + private static native boolean nSetHasOverlappingRendering(long renderNode, boolean hasOverlappingRendering); - private static native void nSetElevation(long renderNode, float lift); - private static native void nSetTranslationX(long renderNode, float translationX); - private static native void nSetTranslationY(long renderNode, float translationY); - private static native void nSetTranslationZ(long renderNode, float translationZ); - private static native void nSetRotation(long renderNode, float rotation); - private static native void nSetRotationX(long renderNode, float rotationX); - private static native void nSetRotationY(long renderNode, float rotationY); - private static native void nSetScaleX(long renderNode, float scaleX); - private static native void nSetScaleY(long renderNode, float scaleY); - private static native void nSetStaticMatrix(long renderNode, long nativeMatrix); - private static native void nSetAnimationMatrix(long renderNode, long animationMatrix); + private static native boolean nSetElevation(long renderNode, float lift); + private static native boolean nSetTranslationX(long renderNode, float translationX); + private static native boolean nSetTranslationY(long renderNode, float translationY); + private static native boolean nSetTranslationZ(long renderNode, float translationZ); + private static native boolean nSetRotation(long renderNode, float rotation); + private static native boolean nSetRotationX(long renderNode, float rotationX); + private static native boolean nSetRotationY(long renderNode, float rotationY); + private static native boolean nSetScaleX(long renderNode, float scaleX); + private static native boolean nSetScaleY(long renderNode, float scaleY); + private static native boolean nSetStaticMatrix(long renderNode, long nativeMatrix); + private static native boolean 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 +957,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 ec4d560..4979059 100644 --- a/core/java/android/view/RenderNodeAnimator.java +++ b/core/java/android/view/RenderNodeAnimator.java @@ -16,12 +16,12 @@ 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 android.util.TimeUtils; import com.android.internal.util.VirtualRefBasePtr; import com.android.internal.view.animation.FallbackLUTInterpolator; @@ -29,12 +29,12 @@ 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; @@ -48,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 @@ -72,36 +79,42 @@ 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 View mViewTarget; private TimeInterpolator mInterpolator; + private boolean mStarted = false; + private boolean mFinished = false; - public int mapViewPropertyToRenderProperty(int viewProperty) { + public static int mapViewPropertyToRenderProperty(int viewProperty) { return sViewPropertyAnimatorMap.get(viewProperty); } - public RenderNodeAnimator(int property, int deltaType, float deltaValue) { + public RenderNodeAnimator(int property, float finalValue) { init(nCreateAnimator(new WeakReference<RenderNodeAnimator>(this), - property, deltaType, deltaValue)); + property, finalValue)); } - public RenderNodeAnimator(CanvasProperty<Float> property, int deltaType, float deltaValue) { + 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) { + /** + * 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)); } private void init(long ptr) { @@ -114,63 +127,159 @@ public final class RenderNodeAnimator { } } + static boolean isNativeInterpolator(TimeInterpolator interpolator) { + return interpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class); + } + private void applyInterpolator() { if (mInterpolator == null) return; long ni; - if (mInterpolator.getClass().isAnnotationPresent(HasNativeInterpolator.class)) { + if (isNativeInterpolator(mInterpolator)) { ni = ((NativeInterpolatorFactory)mInterpolator).createNativeInterpolator(); } else { - int duration = nGetDuration(mNativePtr.get()); + long duration = nGetDuration(mNativePtr.get()); ni = FallbackLUTInterpolator.createNativeInterpolator(mInterpolator, duration); } nSetInterpolator(mNativePtr.get(), ni); } - private void start(RenderNode node) { + @Override + public void start() { + if (mTarget == null) { + throw new IllegalStateException("Missing target!"); + } + if (mStarted) { throw new IllegalStateException("Already started!"); } + mStarted = true; applyInterpolator(); - mTarget = node; 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); + } + } + + @Override + public void end() { + throw new UnsupportedOperationException(); + } + + @Override + public void pause() { + throw new UnsupportedOperationException(); + } + + @Override + public void resume() { + throw new UnsupportedOperationException(); } - public void start(View target) { - start(target.mRenderNode); - // Kick off a frame to start the process - target.invalidateViewProperty(true, false); + public void setTarget(View view) { + mViewTarget = view; + mTarget = view.mRenderNode; } - public void start(Canvas canvas) { + public void setTarget(Canvas canvas) { if (!(canvas instanceof GLES20RecordingCanvas)) { throw new IllegalArgumentException("Not a GLES20RecordingCanvas"); } - GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; - start(recordingCanvas.mNode); + + final GLES20RecordingCanvas recordingCanvas = (GLES20RecordingCanvas) canvas; + setTarget(recordingCanvas.mNode); } - public void cancel() { - mTarget.removeAnimator(this); + 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()); } - public void setDuration(int duration) { + @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; } - long getNativeAnimator() { - return mNativePtr.get(); + @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 @@ -182,12 +291,15 @@ public final class RenderNodeAnimator { } 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 int nGetDuration(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 17035b1..7bbe84e 100644 --- a/core/java/android/view/ThreadedRenderer.java +++ b/core/java/android/view/ThreadedRenderer.java @@ -22,19 +22,19 @@ 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. @@ -54,19 +54,22 @@ import java.io.PrintWriter; public class ThreadedRenderer extends HardwareRenderer { private static final String LOGTAG = "ThreadedRenderer"; - private static final Rect NULL_RECT = new Rect(); - // 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(); @@ -79,6 +82,8 @@ public class ThreadedRenderer extends HardwareRenderer { // Setup timing mChoreographer = Choreographer.getInstance(); nSetFrameInterval(mNativeProxy, mChoreographer.getFrameIntervalNanos()); + + loadSystemProperties(); } @Override @@ -117,7 +122,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) { @@ -145,11 +150,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 @@ -168,19 +173,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) { @@ -193,9 +212,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); @@ -205,20 +226,26 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override - void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks, Rect dirty) { + void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) { attachInfo.mIgnoreDirtyState = true; long frameTimeNanos = mChoreographer.getFrameTimeNanos(); 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, - dirty.left, dirty.top, dirty.right, dirty.bottom); + recordDuration, view.getResources().getDisplayMetrics().density); if ((syncResult & SYNC_INVALIDATE_REQUIRED) != 0) { attachInfo.mViewRootImpl.invalidate(); } @@ -261,12 +288,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 @@ -276,7 +298,7 @@ public class ThreadedRenderer extends HardwareRenderer { @Override void onLayerDestroyed(HardwareLayer layer) { - nDestroyLayer(mNativeProxy, layer.getDeferredLayerUpdater()); + nCancelLayerUpdate(mNativeProxy, layer.getDeferredLayerUpdater()); } @Override @@ -289,6 +311,11 @@ public class ThreadedRenderer extends HardwareRenderer { } @Override + public void notifyFramePending() { + nNotifyFramePending(mNativeProxy); + } + + @Override protected void finalize() throws Throwable { try { nDeleteProxy(mNativeProxy); @@ -298,6 +325,14 @@ public class ThreadedRenderer extends HardwareRenderer { } } + static void startTrimMemory(int level) { + // TODO + } + + static void endTrimMemory() { + // TODO + } + private static class AtlasInitializer { static AtlasInitializer sInstance = new AtlasInitializer(); @@ -334,6 +369,8 @@ public class ThreadedRenderer extends HardwareRenderer { } } + static native void setupShadersDiskCache(String cacheFile); + private static native void nSetAtlas(GraphicBuffer buffer, long[] map); private static native long nCreateRootRenderNode(); @@ -346,12 +383,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, - int dirtyLeft, int dirtyTop, int dirtyRight, int dirtyBottom); + private static native int nSyncAndDrawFrame(long nativeProxy, + long frameTimeNanos, long recordDuration, float density); private static native void nRunWithGlContext(long nativeProxy, Runnable runnable); private static native void nDestroyCanvasAndSurface(long nativeProxy); @@ -360,7 +396,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 e829141..e97a768 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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 @@ -4767,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(); } } @@ -4782,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); } /** @@ -6745,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() @@ -7147,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; @@ -8983,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 @@ -9016,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) { @@ -9051,8 +9075,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } removeTapCallback(); - } else { - clearHotspot(R.attr.state_pressed); } break; @@ -9078,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)) { @@ -9114,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); } } @@ -9165,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); } @@ -9224,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 @@ -10597,24 +10633,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Returns a ValueAnimator which can animate a clipping circle. - * <p> - * The View will be clipped to the animating circle. - * <p> - * Any shadow cast by the View will respect the circular clip from this animator. - * - * @param centerX The x coordinate of the center of the animating circle. - * @param centerY The y coordinate of the center of the animating circle. - * @param startRadius The starting radius of the animating circle. - * @param endRadius The ending radius of the animating circle. - */ - public final ValueAnimator createRevealAnimator(int centerX, int centerY, - float startRadius, float endRadius) { - return RevealAnimator.ofRevealCircle(this, centerX, centerY, - startRadius, endRadius, false); - } - - /** * Returns a ValueAnimator which can animate a clearing circle. * <p> * The View is prevented from drawing within the circle, so the content @@ -10668,24 +10686,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) { @@ -10696,9 +10720,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) { @@ -10707,10 +10756,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); @@ -12869,10 +12918,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(); @@ -13530,12 +13575,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); @@ -13693,6 +13732,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, return; } + renderNode.setScrollPosition(mScrollX, mScrollY); if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || !renderNode.isValid() || (!isLayer && mRecreateDisplayList)) { @@ -16134,6 +16174,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; @@ -19217,8 +19271,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()); } } @@ -19499,7 +19552,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); } } @@ -19723,6 +19775,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/ViewAnimationUtils.java b/core/java/android/view/ViewAnimationUtils.java new file mode 100644 index 0000000..3854f34 --- /dev/null +++ b/core/java/android/view/ViewAnimationUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR 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.RevealAnimator; +import android.animation.ValueAnimator; + +/** + * Defines common utilities for working with View's animations. + * + */ +public class ViewAnimationUtils { + private ViewAnimationUtils() {} + /** + * Returns a ValueAnimator which can animate a clipping circle. + * + * Any shadow cast by the View will respect the circular clip from this animator. + * + * @param view The View will be clipped to the animating circle. + * @param centerX The x coordinate of the center of the animating circle. + * @param centerY The y coordinate of the center of the animating circle. + * @param startRadius The starting radius of the animating circle. + * @param endRadius The ending radius of the animating circle. + */ + public static final ValueAnimator createCircularReveal(View view, + int centerX, int centerY, float startRadius, float endRadius) { + return RevealAnimator.ofRevealCircle(view, centerX, centerY, + startRadius, endRadius, false); + } +} diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 92a42b7..02011e0 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) { @@ -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; @@ -4463,6 +4530,28 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } /** + * Native-calculated damage path + * Returns false if this path was unable to complete successfully. This means + * it hit a ViewParent it doesn't recognize and needs to fall back to calculating + * damage area + * @hide + */ + public boolean damageChildDeferred(View child) { + ViewParent parent = getParent(); + while (parent != null) { + if (parent instanceof ViewGroup) { + parent = parent.getParent(); + } else if (parent instanceof ViewRootImpl) { + ((ViewRootImpl) parent).invalidate(); + return true; + } else { + parent = null; + } + } + return false; + } + + /** * Quick invalidation method called by View.invalidateViewProperty. This doesn't set the * DRAWN flags and doesn't handle the Animation logic that the default invalidation methods * do; all we want to do here is schedule a traversal with the appropriate dirty rect. @@ -4470,6 +4559,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager * @hide */ public void damageChild(View child, final Rect dirty) { + if (damageChildDeferred(child)) { + return; + } + ViewParent parent = this; final AttachInfo attachInfo = mAttachInfo; 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..76d5038 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 @@ -181,7 +181,6 @@ public final class ViewRootImpl implements ViewParent, int mWidth; int mHeight; Rect mDirty; - final Rect mCurrentDirty = new Rect(); boolean mIsAnimating; CompatibilityInfo.Translator mTranslator; @@ -230,6 +229,7 @@ public final class ViewRootImpl implements ViewParent, QueuedInputEvent mPendingInputEventTail; int mPendingInputEventCount; boolean mProcessInputEventsScheduled; + boolean mUnbufferedInputDispatch; String mPendingInputEventQueueLengthCounterName = "pq"; InputStage mFirstInputStage; @@ -668,6 +668,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 +715,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); } @@ -866,7 +860,9 @@ public final class ViewRootImpl implements ViewParent, void invalidate() { mDirty.set(0, 0, mWidth, mHeight); - scheduleTraversals(); + if (!mWillDrawSoon) { + scheduleTraversals(); + } } void invalidateWorld(View view) { @@ -994,13 +990,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 +1718,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; @@ -2426,12 +2437,10 @@ public final class ViewRootImpl implements ViewParent, mHardwareYOffset = yoff; mResizeAlpha = resizeAlpha; - mCurrentDirty.set(dirty); dirty.setEmpty(); mBlockResizeBuffer = false; - attachInfo.mHardwareRenderer.draw(mView, attachInfo, this, - animating ? null : mCurrentDirty); + attachInfo.mHardwareRenderer.draw(mView, attachInfo, this); } else { // If we get here with a disabled & requested hardware renderer, something went // wrong (an invalidate posted right before we destroyed the hardware surface @@ -2447,7 +2456,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 +2607,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 +3154,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 +3491,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 +3889,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 +4013,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 +5286,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 +5339,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 +5547,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 +5698,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 +5738,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 +5784,11 @@ public final class ViewRootImpl implements ViewParent, @Override public void onBatchedInputEventPending() { - scheduleConsumeBatchedInput(); + if (mUnbufferedInputDispatch) { + super.onBatchedInputEventPending(); + } else { + scheduleConsumeBatchedInput(); + } } @Override @@ -5731,6 +5809,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..0ebf2e1 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -147,9 +147,14 @@ public final class WindowManagerGlobal { InputMethodManager imm = InputMethodManager.getInstance(); IWindowManager windowManager = getWindowManagerService(); sWindowSession = windowManager.openSession( + new IWindowSessionCallback.Stub() { + @Override + public void onAnimatorScaleChanged(float scale) { + ValueAnimator.setDurationScale(scale); + } + }, imm.getClient(), imm.getInputContext()); - float animatorScale = windowManager.getAnimationScale(2); - ValueAnimator.setDurationScale(animatorScale); + ValueAnimator.setDurationScale(windowManager.getCurrentAnimatorScale()); } catch (RemoteException e) { Log.e(TAG, "Failed to open window session", e); } @@ -430,7 +435,7 @@ public final class WindowManagerGlobal { HardwareRenderer renderer = root.getView().mAttachInfo.mHardwareRenderer; if (renderer != null) { - renderer.dumpGfxInfo(pw); + renderer.dumpGfxInfo(pw, fd); } } @@ -447,11 +452,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/WindowManagerInternal.java b/core/java/android/view/WindowManagerInternal.java index 14dc356..a92bf59 100644 --- a/core/java/android/view/WindowManagerInternal.java +++ b/core/java/android/view/WindowManagerInternal.java @@ -20,6 +20,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.hardware.display.DisplayManagerInternal; import android.os.IBinder; +import android.os.IRemoteCallback; import java.util.List; @@ -105,7 +106,7 @@ public abstract class WindowManagerInternal { * Set by the accessibility layer to specify the magnification and panning to * be applied to all windows that should be magnified. * - * @param callbacks The callbacks to invoke. + * @param spec The MagnficationSpec to set. * * @see #setMagnificationCallbacks(MagnificationCallbacks) */ @@ -161,4 +162,10 @@ public abstract class WindowManagerInternal { * @param outBounds The frame to populate. */ public abstract void getWindowFrame(IBinder token, Rect outBounds); + + /** + * Invalidate all visible windows. Then report back on the callback once all windows have + * redrawn. + */ + public abstract void waitForAllWindowsDrawn(IRemoteCallback callback, long timeout); } 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/accessibility/CaptioningManager.java b/core/java/android/view/accessibility/CaptioningManager.java index a0134d6..334ff43 100644 --- a/core/java/android/view/accessibility/CaptioningManager.java +++ b/core/java/android/view/accessibility/CaptioningManager.java @@ -16,6 +16,8 @@ package android.view.accessibility; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.ContentResolver; import android.content.Context; import android.database.ContentObserver; @@ -78,6 +80,7 @@ public class CaptioningManager { * language * @hide */ + @Nullable public final String getRawLocale() { return Secure.getString(mContentResolver, Secure.ACCESSIBILITY_CAPTIONING_LOCALE); } @@ -86,6 +89,7 @@ public class CaptioningManager { * @return the locale for the user's preferred captioning language, or null * if not specified */ + @Nullable public final Locale getLocale() { final String rawLocale = getRawLocale(); if (!TextUtils.isEmpty(rawLocale)) { @@ -125,6 +129,7 @@ public class CaptioningManager { * @return the user's preferred visual properties for captions as a * {@link CaptionStyle}, or the default style if not specified */ + @NonNull public CaptionStyle getUserStyle() { final int preset = getRawUserStyle(); if (preset == CaptionStyle.PRESET_CUSTOM) { @@ -140,17 +145,19 @@ public class CaptioningManager { * * @param listener the listener to add */ - public void addCaptioningChangeListener(CaptioningChangeListener listener) { + public void addCaptioningChangeListener(@NonNull CaptioningChangeListener listener) { synchronized (mListeners) { if (mListeners.isEmpty()) { registerObserver(Secure.ACCESSIBILITY_CAPTIONING_ENABLED); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_WINDOW_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_TYPE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_EDGE_COLOR); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_TYPEFACE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_FONT_SCALE); registerObserver(Secure.ACCESSIBILITY_CAPTIONING_LOCALE); + registerObserver(Secure.ACCESSIBILITY_CAPTIONING_PRESET); } mListeners.add(listener); @@ -167,7 +174,7 @@ public class CaptioningManager { * * @param listener the listener to remove */ - public void removeCaptioningChangeListener(CaptioningChangeListener listener) { + public void removeCaptioningChangeListener(@NonNull CaptioningChangeListener listener) { synchronized (mListeners) { mListeners.remove(listener); @@ -253,11 +260,18 @@ public class CaptioningManager { /** Packed value for a color of 'none' and a cached opacity of 100%. */ private static final int COLOR_NONE_OPAQUE = 0x000000FF; + /** Packed value for an unspecified color and opacity. */ + private static final int COLOR_UNSPECIFIED = 0x000001FF; + private static final CaptionStyle WHITE_ON_BLACK; private static final CaptionStyle BLACK_ON_WHITE; private static final CaptionStyle YELLOW_ON_BLACK; private static final CaptionStyle YELLOW_ON_BLUE; private static final CaptionStyle DEFAULT_CUSTOM; + private static final CaptionStyle UNSPECIFIED; + + /** The default caption style used to fill in unspecified values. @hide */ + public static final CaptionStyle DEFAULT; /** @hide */ public static final CaptionStyle[] PRESETS; @@ -265,6 +279,9 @@ public class CaptioningManager { /** @hide */ public static final int PRESET_CUSTOM = -1; + /** Unspecified edge type value. */ + public static final int EDGE_TYPE_UNSPECIFIED = -1; + /** Edge type value specifying no character edges. */ public static final int EDGE_TYPE_NONE = 0; @@ -289,6 +306,7 @@ public class CaptioningManager { /** * The preferred edge type for video captions, one of: * <ul> + * <li>{@link #EDGE_TYPE_UNSPECIFIED} * <li>{@link #EDGE_TYPE_NONE} * <li>{@link #EDGE_TYPE_OUTLINE} * <li>{@link #EDGE_TYPE_DROP_SHADOW} @@ -326,9 +344,81 @@ public class CaptioningManager { } /** + * Applies a caption style, overriding any properties that are specified + * in the overlay caption. + * + * @param overlay The style to apply + * @return A caption style with the overlay style applied + * @hide + */ + @NonNull + public CaptionStyle applyStyle(@NonNull CaptionStyle overlay) { + final int newForegroundColor = overlay.hasForegroundColor() ? + overlay.foregroundColor : foregroundColor; + final int newBackgroundColor = overlay.hasBackgroundColor() ? + overlay.backgroundColor : backgroundColor; + final int newEdgeType = overlay.hasEdgeType() ? + overlay.edgeType : edgeType; + final int newEdgeColor = overlay.hasEdgeColor() ? + overlay.edgeColor : edgeColor; + final int newWindowColor = overlay.hasWindowColor() ? + overlay.windowColor : windowColor; + final String newRawTypeface = overlay.mRawTypeface != null ? + overlay.mRawTypeface : mRawTypeface; + return new CaptionStyle(newForegroundColor, newBackgroundColor, newEdgeType, + newEdgeColor, newWindowColor, newRawTypeface); + } + + /** + * @return {@code true} if the user has specified a background color + * that should override the application default, {@code false} + * otherwise + */ + public boolean hasBackgroundColor() { + return backgroundColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified a foreground color + * that should override the application default, {@code false} + * otherwise + */ + public boolean hasForegroundColor() { + return foregroundColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified an edge type that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasEdgeType() { + return edgeType != EDGE_TYPE_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified an edge color that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasEdgeColor() { + return edgeColor != COLOR_UNSPECIFIED; + } + + /** + * @return {@code true} if the user has specified a window color that + * should override the application default, {@code false} + * otherwise + */ + public boolean hasWindowColor() { + return windowColor != COLOR_UNSPECIFIED; + } + + /** * @return the preferred {@link Typeface} for video captions, or null if * not specified */ + @Nullable public Typeface getTypeface() { if (mParsedTypeface == null && !TextUtils.isEmpty(mRawTypeface)) { mParsedTypeface = Typeface.create(mRawTypeface, Typeface.NORMAL); @@ -339,6 +429,7 @@ public class CaptioningManager { /** * @hide */ + @NonNull public static CaptionStyle getCustomStyle(ContentResolver cr) { final CaptionStyle defStyle = CaptionStyle.DEFAULT_CUSTOM; final int foregroundColor = Secure.getInt( @@ -370,12 +461,17 @@ public class CaptioningManager { Color.BLACK, COLOR_NONE_OPAQUE, null); YELLOW_ON_BLUE = new CaptionStyle(Color.YELLOW, Color.BLUE, EDGE_TYPE_NONE, Color.BLACK, COLOR_NONE_OPAQUE, null); + UNSPECIFIED = new CaptionStyle(COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, + EDGE_TYPE_UNSPECIFIED, COLOR_UNSPECIFIED, COLOR_UNSPECIFIED, null); + // The ordering of these cannot change since we store the index + // directly in preferences. PRESETS = new CaptionStyle[] { - WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE + WHITE_ON_BLACK, BLACK_ON_WHITE, YELLOW_ON_BLACK, YELLOW_ON_BLUE, UNSPECIFIED }; DEFAULT_CUSTOM = WHITE_ON_BLACK; + DEFAULT = WHITE_ON_BLACK; } } @@ -389,8 +485,7 @@ public class CaptioningManager { * * @param enabled the user's new preferred captioning enabled state */ - public void onEnabledChanged(boolean enabled) { - } + public void onEnabledChanged(boolean enabled) {} /** * Called when the captioning user style changes. @@ -398,17 +493,15 @@ public class CaptioningManager { * @param userStyle the user's new preferred style * @see CaptioningManager#getUserStyle() */ - public void onUserStyleChanged(CaptionStyle userStyle) { - } + public void onUserStyleChanged(@NonNull CaptionStyle userStyle) {} /** * Called when the captioning locale changes. * - * @param locale the preferred captioning locale + * @param locale the preferred captioning locale, or {@code null} if not specified * @see CaptioningManager#getLocale() */ - public void onLocaleChanged(Locale locale) { - } + public void onLocaleChanged(@Nullable Locale locale) {} /** * Called when the captioning font scaling factor changes. @@ -416,7 +509,6 @@ public class CaptioningManager { * @param fontScale the preferred font scaling factor * @see CaptioningManager#getFontScale() */ - public void onFontScaleChanged(float fontScale) { - } + public void onFontScaleChanged(float fontScale) {} } } diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index cccfa78..a74e3a0 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -602,8 +602,7 @@ public class BaseInputConnection implements InputConnection { beginBatchEdit(); if (!composing && !TextUtils.isEmpty(text)) { - // Notify the text is committed by the user to InputMethodManagerService - mIMM.notifyTextCommitted(); + mIMM.notifyUserAction(); } // delete composing text set previously. 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 f874eb7..0693617 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -363,7 +363,7 @@ public final class InputMethodManager { static final int MSG_SEND_INPUT_EVENT = 5; static final int MSG_TIMEOUT_INPUT_EVENT = 6; static final int MSG_FLUSH_INPUT_EVENT = 7; - static final int SET_CURSOR_ANCHOR_MONITOR_MODE = 8; + static final int MSG_SET_CURSOR_ANCHOR_MONITOR_MODE = 8; class H extends Handler { H(Looper looper) { @@ -494,7 +494,7 @@ public final class InputMethodManager { finishedInputEvent(msg.arg1, false, false); return; } - case SET_CURSOR_ANCHOR_MONITOR_MODE: { + case MSG_SET_CURSOR_ANCHOR_MONITOR_MODE: { synchronized (mH) { mCursorAnchorMonitorMode = msg.arg1; // Clear the cache. @@ -570,7 +570,7 @@ public final class InputMethodManager { @Override public void setCursorAnchorMonitorMode(int monitorMode) { - mH.sendMessage(mH.obtainMessage(SET_CURSOR_ANCHOR_MONITOR_MODE, monitorMode, 0)); + mH.sendMessage(mH.obtainMessage(MSG_SET_CURSOR_ANCHOR_MONITOR_MODE, monitorMode, 0)); } }; @@ -1913,13 +1913,13 @@ public final class InputMethodManager { } /** - * Notify the current IME commits text + * Notify that a user took some action with this input method. * @hide */ - public void notifyTextCommitted() { + public void notifyUserAction() { synchronized (mH) { try { - mService.notifyTextCommitted(); + mService.notifyUserAction(); } 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/CookieManager.java b/core/java/android/webkit/CookieManager.java index 2b75d83..abed082 100644 --- a/core/java/android/webkit/CookieManager.java +++ b/core/java/android/webkit/CookieManager.java @@ -70,8 +70,7 @@ public class CookieManager { /** * Sets a cookie for the given URL. Any existing cookie with the same host, * path and name will be replaced with the new cookie. The cookie being set - * must not have expired and must not be a session cookie, otherwise it - * will be ignored. + * will be ignored if it is expired. * * @param url the URL for which the cookie is set * @param value the cookie as a string, using the format of the 'Set-Cookie' 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 ac12357..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; @@ -88,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/webkit/WebViewFactoryProvider.java b/core/java/android/webkit/WebViewFactoryProvider.java index 945e0e3..6e6a987 100644 --- a/core/java/android/webkit/WebViewFactoryProvider.java +++ b/core/java/android/webkit/WebViewFactoryProvider.java @@ -37,13 +37,6 @@ public interface WebViewFactoryProvider { String findAddress(String addr); /** - * Implements the API methods: - * {@link android.webkit.WebView#enablePlatformNotifications()} - * {@link android.webkit.WebView#disablePlatformNotifications()} - */ - void setPlatformNotificationsEnabled(boolean enable); - - /** * Implements the API method: * {@link android.webkit.WebSettings#getDefaultUserAgent(Context) } */ diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index f91ef1a..372228c 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -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) { @@ -3783,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); @@ -3797,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(); } @@ -4028,12 +4028,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te final int scrollY = mScrollY; if (!mEdgeGlowTop.isFinished()) { final int restoreCount = canvas.save(); - final int leftPadding = mListPadding.left + mGlowPaddingLeft; - final int rightPadding = mListPadding.right + mGlowPaddingRight; - final int width = getWidth() - leftPadding - rightPadding; + final int width = getWidth(); int edgeY = Math.min(0, scrollY + mFirstPositionDistanceGuess); - canvas.translate(leftPadding, edgeY); + canvas.translate(0, edgeY); mEdgeGlowTop.setSize(width, getHeight()); if (mEdgeGlowTop.draw(canvas)) { invalidate(0, 0, getWidth(), @@ -4043,12 +4041,10 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te } if (!mEdgeGlowBottom.isFinished()) { final int restoreCount = canvas.save(); - final int leftPadding = mListPadding.left + mGlowPaddingLeft; - final int rightPadding = mListPadding.right + mGlowPaddingRight; - final int width = getWidth() - leftPadding - rightPadding; + final int width = getWidth(); final int height = getHeight(); - int edgeX = -width + leftPadding; + int edgeX = -width; int edgeY = Math.max(height, scrollY + mLastPositionDistanceGuess); canvas.translate(edgeX, edgeY); canvas.rotate(180, width, 0); diff --git a/core/java/android/widget/AbsSeekBar.java b/core/java/android/widget/AbsSeekBar.java index 4f2d9c6..43f623b 100644 --- a/core/java/android/widget/AbsSeekBar.java +++ b/core/java/android/widget/AbsSeekBar.java @@ -31,8 +31,6 @@ 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(); @@ -258,6 +256,16 @@ public abstract class AbsSeekBar extends ProgressBar { } @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); @@ -348,7 +356,7 @@ public abstract class AbsSeekBar extends ProgressBar { 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; @@ -499,17 +507,10 @@ public abstract class AbsSeekBar extends ProgressBar { return true; } - private void setHotspot(int id, float x, float y) { - final Drawable bg = getBackground(); - if (bg != null && bg.supportsHotspots()) { - bg.setHotspot(id, x, y); - } - } - - private void clearHotspot(int id) { + private void setHotspot(float x, float y) { final Drawable bg = getBackground(); - if (bg != null && bg.supportsHotspots()) { - bg.removeHotspot(id); + if (bg != null) { + bg.setHotspot(x, y); } } @@ -541,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); } @@ -567,7 +568,6 @@ public abstract class AbsSeekBar extends ProgressBar { * canceled. */ void onStopTrackingTouch() { - clearHotspot(R.attr.state_pressed); mIsDragging = false; } 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 c4a40b4..2502954 100644 --- a/core/java/android/widget/EdgeEffect.java +++ b/core/java/android/widget/EdgeEffect.java @@ -122,7 +122,7 @@ public class EdgeEffect { final TypedArray a = context.obtainStyledAttributes( com.android.internal.R.styleable.EdgeEffect); final int themeColor = a.getColor( - com.android.internal.R.styleable.EdgeEffect_colorPrimaryLight, 0xff666666); + com.android.internal.R.styleable.EdgeEffect_colorPrimary, 0xff666666); a.recycle(); mPaint.setColor((themeColor & 0xffffff) | 0x33000000); mPaint.setStyle(Paint.Style.FILL); @@ -312,8 +312,7 @@ public class EdgeEffect { final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f; float translateX = mBounds.width() * displacement / 2; - canvas.clipRect(Float.MIN_VALUE, mBounds.top, - Float.MAX_VALUE, Float.MAX_VALUE); + canvas.clipRect(mBounds); canvas.translate(translateX, 0); canvas.drawArc(mArcRect, 45, 90, true, mPaint); canvas.restoreToCount(count); diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index cbe7511..27d6b82 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -3041,8 +3041,7 @@ public class Editor { builder.reset(); final int selectionStart = mTextView.getSelectionStart(); - final int selectionEnd = mTextView.getSelectionEnd(); - builder.setSelectionRange(mTextView.getSelectionStart(), mTextView.getSelectionEnd()); + builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd()); // Construct transformation matrix from view local coordinates to screen coordinates. mViewToScreenMatrix.set(mTextView.getMatrix()); @@ -3055,17 +3054,24 @@ public class Editor { final float viewportToContentVerticalOffset = mTextView.viewportToContentVerticalOffset(); - if (mTextView.getText() instanceof Spannable) { - final Spannable sp = (Spannable) mTextView.getText(); - int compositionStart = EditableInputConnection.getComposingSpanStart(sp); - int compositionEnd = EditableInputConnection.getComposingSpanEnd(sp); - if (compositionEnd < compositionStart) { - final int temp = compositionEnd; - compositionEnd = compositionStart; - compositionStart = temp; + 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); } - builder.setCandidateRange(compositionStart, compositionEnd); - for (int offset = compositionStart; offset < compositionEnd; offset++) { + for (int offset = composingTextStart; offset < composingTextEnd; offset++) { if (offset < 0) { continue; } 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/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 0c3715d..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( diff --git a/core/java/android/widget/RadialTimePickerView.java b/core/java/android/widget/RadialTimePickerView.java index 1c9ab61..bb74c44 100644 --- a/core/java/android/widget/RadialTimePickerView.java +++ b/core/java/android/widget/RadialTimePickerView.java @@ -42,6 +42,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; + import com.android.internal.R; import java.text.DateFormatSymbols; @@ -82,13 +83,13 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private static final int ALPHA_TRANSPARENT = 0; // Alpha level of color for selector. - private static final int ALPHA_SELECTOR = 51; + private static final int ALPHA_SELECTOR = 255; // was 51 // Alpha level of color for selected circle. private static final int ALPHA_AMPM_SELECTED = ALPHA_SELECTOR; // Alpha level of color for pressed circle. - private static final int ALPHA_AMPM_PRESSED = 175; + private static final int ALPHA_AMPM_PRESSED = 255; // was 175 private static final float COSINE_30_DEGREES = ((float) Math.sqrt(3)) * 0.5f; private static final float SINE_30_DEGREES = 0.5f; @@ -112,8 +113,15 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { private final String[] mAmPmText = new String[2]; private final Paint[] mPaint = new Paint[2]; + private final int[] mColor = new int[2]; + private final IntHolder[] mAlpha = new IntHolder[2]; + private final Paint mPaintCenter = new Paint(); + private final Paint[][] mPaintSelector = new Paint[2][3]; + private final int[][] mColorSelector = new int[2][3]; + private final IntHolder[][] mAlphaSelector = new IntHolder[2][3]; + private final Paint mPaintAmPmText = new Paint(); private final Paint[] mPaintAmPmCircle = new Paint[2]; @@ -309,65 +317,80 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { final Resources res = getResources(); mAmPmUnselectedColor = a.getColor(R.styleable.TimePicker_amPmUnselectedBackgroundColor, - res.getColor( - R.color.timepicker_default_ampm_unselected_background_color_holo_light)); + res.getColor(R.color.timepicker_default_ampm_unselected_background_color_quantum)); mAmPmSelectedColor = a.getColor(R.styleable.TimePicker_amPmSelectedBackgroundColor, - res.getColor(R.color.timepicker_default_ampm_selected_background_color_holo_light)); + res.getColor(R.color.timepicker_default_ampm_selected_background_color_quantum)); mAmPmTextColor = a.getColor(R.styleable.TimePicker_amPmTextColor, - res.getColor(R.color.timepicker_default_text_color_holo_light)); - - final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, - res.getColor(R.color.timepicker_default_text_color_holo_light)); + res.getColor(R.color.timepicker_default_text_color_quantum)); mTypeface = Typeface.create("sans-serif", Typeface.NORMAL); + // Initialize all alpha values to opaque. + for (int i = 0; i < mAlpha.length; i++) { + mAlpha[i] = new IntHolder(ALPHA_OPAQUE); + } + for (int i = 0; i < mAlphaSelector.length; i++) { + for (int j = 0; j < mAlphaSelector[i].length; j++) { + mAlphaSelector[i][j] = new IntHolder(ALPHA_OPAQUE); + } + } + + final int numbersTextColor = a.getColor(R.styleable.TimePicker_numbersTextColor, + res.getColor(R.color.timepicker_default_text_color_quantum)); + mPaint[HOURS] = new Paint(); - mPaint[HOURS].setColor(numbersTextColor); mPaint[HOURS].setAntiAlias(true); mPaint[HOURS].setTextAlign(Paint.Align.CENTER); + mColor[HOURS] = numbersTextColor; mPaint[MINUTES] = new Paint(); - mPaint[MINUTES].setColor(numbersTextColor); mPaint[MINUTES].setAntiAlias(true); mPaint[MINUTES].setTextAlign(Paint.Align.CENTER); + mColor[MINUTES] = numbersTextColor; mPaintCenter.setColor(numbersTextColor); mPaintCenter.setAntiAlias(true); mPaintCenter.setTextAlign(Paint.Align.CENTER); mPaintSelector[HOURS][SELECTOR_CIRCLE] = new Paint(); - mPaintSelector[HOURS][SELECTOR_CIRCLE].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[HOURS][SELECTOR_CIRCLE].setAntiAlias(true); + mColorSelector[HOURS][SELECTOR_CIRCLE] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintSelector[HOURS][SELECTOR_DOT] = new Paint(); - mPaintSelector[HOURS][SELECTOR_DOT].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[HOURS][SELECTOR_DOT].setAntiAlias(true); + mColorSelector[HOURS][SELECTOR_DOT] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintSelector[HOURS][SELECTOR_LINE] = new Paint(); - mPaintSelector[HOURS][SELECTOR_LINE].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[HOURS][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[HOURS][SELECTOR_LINE].setStrokeWidth(2); + mColorSelector[HOURS][SELECTOR_LINE] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintSelector[MINUTES][SELECTOR_CIRCLE] = new Paint(); - mPaintSelector[MINUTES][SELECTOR_CIRCLE].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAntiAlias(true); + mColorSelector[MINUTES][SELECTOR_CIRCLE] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintSelector[MINUTES][SELECTOR_DOT] = new Paint(); - mPaintSelector[MINUTES][SELECTOR_DOT].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[MINUTES][SELECTOR_DOT].setAntiAlias(true); + mColorSelector[MINUTES][SELECTOR_DOT] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintSelector[MINUTES][SELECTOR_LINE] = new Paint(); - mPaintSelector[MINUTES][SELECTOR_LINE].setColor( - a.getColor(R.styleable.TimePicker_numbersSelectorColor, R.color.holo_blue_light)); mPaintSelector[MINUTES][SELECTOR_LINE].setAntiAlias(true); mPaintSelector[MINUTES][SELECTOR_LINE].setStrokeWidth(2); + mColorSelector[MINUTES][SELECTOR_LINE] = a.getColor( + R.styleable.TimePicker_numbersSelectorColor, + R.color.timepicker_default_selector_color_quantum); mPaintAmPmText.setColor(mAmPmTextColor); mPaintAmPmText.setTypeface(mTypeface); @@ -379,13 +402,12 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mPaintAmPmCircle[PM] = new Paint(); mPaintAmPmCircle[PM].setAntiAlias(true); - mPaintBackground.setColor( - a.getColor(R.styleable.TimePicker_numbersBackgroundColor, Color.WHITE)); + mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor, + res.getColor(R.color.timepicker_default_numbers_background_color_quantum))); mPaintBackground.setAntiAlias(true); - final int disabledColor = a.getColor(R.styleable.TimePicker_disabledColor, - res.getColor(R.color.timepicker_default_disabled_color_holo_light)); - mPaintDisabled.setColor(disabledColor); + mPaintDisabled.setColor(a.getColor(R.styleable.TimePicker_disabledColor, + res.getColor(R.color.timepicker_default_disabled_color_quantum))); mPaintDisabled.setAntiAlias(true); if (DEBUG) { @@ -415,6 +437,8 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mSelectionRadiusMultiplier = Float.parseFloat( res.getString(R.string.timepicker_selection_radius_multiplier)); + a.recycle(); + setOnTouchListener(this); // Initial values @@ -622,21 +646,21 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mAmPmCircleRadiusMultiplier = Float.parseFloat( res.getString(R.string.timepicker_ampm_circle_radius_multiplier)); - mPaint[HOURS].setAlpha(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); - mPaint[MINUTES].setAlpha(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); + mAlpha[HOURS].setValue(mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); + mAlpha[MINUTES].setValue(mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); - mPaintSelector[HOURS][SELECTOR_CIRCLE].setAlpha( - mShowHours ?ALPHA_SELECTOR : ALPHA_TRANSPARENT); - mPaintSelector[HOURS][SELECTOR_DOT].setAlpha( + mAlphaSelector[HOURS][SELECTOR_CIRCLE].setValue( + mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); + mAlphaSelector[HOURS][SELECTOR_DOT].setValue( mShowHours ? ALPHA_OPAQUE : ALPHA_TRANSPARENT); - mPaintSelector[HOURS][SELECTOR_LINE].setAlpha( + mAlphaSelector[HOURS][SELECTOR_LINE].setValue( mShowHours ? ALPHA_SELECTOR : ALPHA_TRANSPARENT); - mPaintSelector[MINUTES][SELECTOR_CIRCLE].setAlpha( + mAlphaSelector[MINUTES][SELECTOR_CIRCLE].setValue( mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); - mPaintSelector[MINUTES][SELECTOR_DOT].setAlpha( + mAlphaSelector[MINUTES][SELECTOR_DOT].setValue( mShowHours ? ALPHA_TRANSPARENT : ALPHA_OPAQUE); - mPaintSelector[MINUTES][SELECTOR_LINE].setAlpha( + mAlphaSelector[MINUTES][SELECTOR_LINE].setValue( mShowHours ? ALPHA_TRANSPARENT : ALPHA_SELECTOR); } @@ -704,20 +728,23 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { calculateGridSizesMinutes(); drawCircleBackground(canvas); + drawSelector(canvas); drawTextElements(canvas, mTextSize[HOURS], mTypeface, mOuterTextHours, - mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS]); + mTextGridWidths[HOURS], mTextGridHeights[HOURS], mPaint[HOURS], + mColor[HOURS], mAlpha[HOURS].getValue()); if (mIs24HourMode && mInnerTextHours != null) { drawTextElements(canvas, mInnerTextSize, mTypeface, mInnerTextHours, - mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS]); + mInnerTextGridWidths, mInnerTextGridHeights, mPaint[HOURS], + mColor[HOURS], mAlpha[HOURS].getValue()); } drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mOuterTextMinutes, - mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES]); + mTextGridWidths[MINUTES], mTextGridHeights[MINUTES], mPaint[MINUTES], + mColor[MINUTES], mAlpha[MINUTES].getValue()); drawCenter(canvas); - drawSelector(canvas); if (!mIs24HourMode) { drawAmPm(canvas); } @@ -772,12 +799,12 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { // Draw the two circles mPaintAmPmCircle[AM].setColor(amColor); - mPaintAmPmCircle[AM].setAlpha(amAlpha); + mPaintAmPmCircle[AM].setAlpha(getMultipliedAlpha(amColor, amAlpha)); canvas.drawCircle(isLayoutRtl ? mRightIndicatorXCenter : mLeftIndicatorXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[AM]); mPaintAmPmCircle[PM].setColor(pmColor); - mPaintAmPmCircle[PM].setAlpha(pmAlpha); + mPaintAmPmCircle[PM].setAlpha(getMultipliedAlpha(pmColor, pmAlpha)); canvas.drawCircle(isLayoutRtl ? mLeftIndicatorXCenter : mRightIndicatorXCenter, mAmPmYCenter, mAmPmCircleRadius, mPaintAmPmCircle[PM]); @@ -792,6 +819,10 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { textYCenter, mPaintAmPmText); } + private int getMultipliedAlpha(int argb, int alpha) { + return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5); + } + private void drawSelector(Canvas canvas, int index) { // Calculate the current radius at which to place the selection circle. mLineLength[index] = (int) (mCircleRadius[index] @@ -802,15 +833,27 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { int pointX = mXCenter + (int) (mLineLength[index] * Math.sin(selectionRadians)); int pointY = mYCenter - (int) (mLineLength[index] * Math.cos(selectionRadians)); + int color; + int alpha; + Paint paint; + // Draw the selection circle - canvas.drawCircle(pointX, pointY, mSelectionRadius[index], - mPaintSelector[index % 2][SELECTOR_CIRCLE]); + color = mColorSelector[index % 2][SELECTOR_CIRCLE]; + alpha = mAlphaSelector[index % 2][SELECTOR_CIRCLE].getValue(); + paint = mPaintSelector[index % 2][SELECTOR_CIRCLE]; + paint.setColor(color); + paint.setAlpha(getMultipliedAlpha(color, alpha)); + canvas.drawCircle(pointX, pointY, mSelectionRadius[index], paint); // Draw the dot if needed if (mSelectionDegrees[index] % 30 != 0) { // We're not on a direct tick - canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), - mPaintSelector[index % 2][SELECTOR_DOT]); + color = mColorSelector[index % 2][SELECTOR_DOT]; + alpha = mAlphaSelector[index % 2][SELECTOR_DOT].getValue(); + paint = mPaintSelector[index % 2][SELECTOR_DOT]; + paint.setColor(color); + paint.setAlpha(getMultipliedAlpha(color, alpha)); + canvas.drawCircle(pointX, pointY, (mSelectionRadius[index] * 2 / 7), paint); } else { // We're not drawing the dot, so shorten the line to only go as far as the edge of the // selection circle @@ -820,8 +863,12 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { } // Draw the line - canvas.drawLine(mXCenter, mYCenter, pointX, pointY, - mPaintSelector[index % 2][SELECTOR_LINE]); + color = mColorSelector[index % 2][SELECTOR_LINE]; + alpha = mAlphaSelector[index % 2][SELECTOR_LINE].getValue(); + paint = mPaintSelector[index % 2][SELECTOR_LINE]; + paint.setColor(color); + paint.setAlpha(getMultipliedAlpha(color, alpha)); + canvas.drawLine(mXCenter, mYCenter, pointX, pointY, paint); } private void drawDebug(Canvas canvas) { @@ -948,9 +995,11 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { * Draw the 12 text values at the positions specified by the textGrid parameters. */ private void drawTextElements(Canvas canvas, float textSize, Typeface typeface, String[] texts, - float[] textGridWidths, float[] textGridHeights, Paint paint) { + float[] textGridWidths, float[] textGridHeights, Paint paint, int color, int alpha) { paint.setTextSize(textSize); paint.setTypeface(typeface); + paint.setColor(color); + paint.setAlpha(getMultipliedAlpha(color, alpha)); canvas.drawText(texts[0], textGridWidths[3], textGridHeights[0], paint); canvas.drawText(texts[1], textGridWidths[4], textGridHeights[1], paint); canvas.drawText(texts[2], textGridWidths[5], textGridHeights[2], paint); @@ -1023,17 +1072,17 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { return animator; } - private static ObjectAnimator getFadeOutAnimator(Object target, int startAlpha, int endAlpha, + private static ObjectAnimator getFadeOutAnimator(IntHolder target, int startAlpha, int endAlpha, InvalidateUpdateListener updateListener) { int duration = 500; - ObjectAnimator animator = ObjectAnimator.ofInt(target, "alpha", startAlpha, endAlpha); + ObjectAnimator animator = ObjectAnimator.ofInt(target, "value", startAlpha, endAlpha); animator.setDuration(duration); animator.addUpdateListener(updateListener); return animator; } - private static ObjectAnimator getFadeInAnimator(Object target, int startAlpha, int endAlpha, + private static ObjectAnimator getFadeInAnimator(IntHolder target, int startAlpha, int endAlpha, InvalidateUpdateListener updateListener) { Keyframe kf0, kf1, kf2; int duration = 500; @@ -1048,7 +1097,7 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { kf0 = Keyframe.ofInt(0f, startAlpha); kf1 = Keyframe.ofInt(delayPoint, startAlpha); kf2 = Keyframe.ofInt(1f, endAlpha); - PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("alpha", kf0, kf1, kf2); + PropertyValuesHolder fadeIn = PropertyValuesHolder.ofKeyframe("value", kf0, kf1, kf2); ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder( target, fadeIn).setDuration(totalDuration); @@ -1068,25 +1117,25 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mHoursToMinutesAnims.add(getRadiusDisappearAnimator(this, "animationRadiusMultiplierHours", mInvalidateUpdateListener, mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mPaint[HOURS], + mHoursToMinutesAnims.add(getFadeOutAnimator(mAlpha[HOURS], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeOutAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + mHoursToMinutesAnims.add(getFadeOutAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mHoursToMinutesAnims.add(getRadiusReappearAnimator(this, "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); - mHoursToMinutesAnims.add(getFadeInAnimator(mPaint[MINUTES], + mHoursToMinutesAnims.add(getFadeInAnimator(mAlpha[MINUTES], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mHoursToMinutesAnims.add(getFadeInAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + mHoursToMinutesAnims.add(getFadeInAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); } @@ -1103,25 +1152,25 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mMinuteToHoursAnims.add(getRadiusDisappearAnimator(this, "animationRadiusMultiplierMinutes", mInvalidateUpdateListener, mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mPaint[MINUTES], + mMinuteToHoursAnims.add(getFadeOutAnimator(mAlpha[MINUTES], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_CIRCLE], + mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_CIRCLE], ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_DOT], + mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_DOT], ALPHA_OPAQUE, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeOutAnimator(mPaintSelector[MINUTES][SELECTOR_LINE], + mMinuteToHoursAnims.add(getFadeOutAnimator(mAlphaSelector[MINUTES][SELECTOR_LINE], ALPHA_SELECTOR, ALPHA_TRANSPARENT, mInvalidateUpdateListener)); mMinuteToHoursAnims.add(getRadiusReappearAnimator(this, "animationRadiusMultiplierHours", mInvalidateUpdateListener, mTransitionMidRadiusMultiplier, mTransitionEndRadiusMultiplier)); - mMinuteToHoursAnims.add(getFadeInAnimator(mPaint[HOURS], + mMinuteToHoursAnims.add(getFadeInAnimator(mAlpha[HOURS], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_CIRCLE], + mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_CIRCLE], ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_DOT], + mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_DOT], ALPHA_TRANSPARENT, ALPHA_OPAQUE, mInvalidateUpdateListener)); - mMinuteToHoursAnims.add(getFadeInAnimator(mPaintSelector[HOURS][SELECTOR_LINE], + mMinuteToHoursAnims.add(getFadeInAnimator(mAlphaSelector[HOURS][SELECTOR_LINE], ALPHA_TRANSPARENT, ALPHA_SELECTOR, mInvalidateUpdateListener)); } @@ -1393,4 +1442,20 @@ public class RadialTimePickerView extends View implements View.OnTouchListener { mInputEnabled = inputEnabled; invalidate(); } + + private static class IntHolder { + private int mValue; + + public IntHolder(int value) { + mValue = value; + } + + public void setValue(int value) { + mValue = value; + } + + public int getValue() { + return mValue; + } + } } 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/ShareActionProvider.java b/core/java/android/widget/ShareActionProvider.java index 99a7886..ac79d05 100644 --- a/core/java/android/widget/ShareActionProvider.java +++ b/core/java/android/widget/ShareActionProvider.java @@ -280,6 +280,7 @@ public class ShareActionProvider extends ActionProvider { final String action = shareIntent.getAction(); if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_AUTO_REMOVE_FROM_RECENTS); } } @@ -303,6 +304,7 @@ public class ShareActionProvider extends ActionProvider { if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) { launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_AUTO_REMOVE_FROM_RECENTS); } mContext.startActivity(launchIntent); 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 438e164..c5c6e64 100644 --- a/core/java/android/widget/Switch.java +++ b/core/java/android/widget/Switch.java @@ -666,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; @@ -801,7 +803,7 @@ 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; @@ -815,9 +817,6 @@ 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(); @@ -829,12 +828,30 @@ public class Switch extends CompoundButton { 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); + 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); @@ -861,7 +878,8 @@ public class Switch extends CompoundButton { } mTextPaint.drawableState = drawableState; - final int left = (thumbLeft + thumbRight) / 2 - switchText.getWidth() / 2; + 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); @@ -933,9 +951,8 @@ public class Switch extends CompoundButton { final int[] myDrawableState = getDrawableState(); - if (mThumbDrawable != null && mThumbDrawable.setState(myDrawableState)) { - // Handle changes to thumb width and height. - requestLayout(); + if (mThumbDrawable != null) { + mThumbDrawable.setState(myDrawableState); } if (mTrackDrawable != null) { @@ -946,6 +963,16 @@ public class Switch extends CompoundButton { } @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..43c8dde 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -222,6 +222,7 @@ import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1; * @attr ref android.R.styleable#TextView_imeActionLabel * @attr ref android.R.styleable#TextView_imeActionId * @attr ref android.R.styleable#TextView_editorExtras + * @attr ref android.R.styleable#TextView_elegantTextHeight */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { @@ -2637,6 +2638,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * metrics, and also increases top and bottom bounds to provide more space. * * @param elegant set the paint's elegant metrics flag. + * + * @attr ref android.R.styleable#TextView_elegantTextHeight */ public void setElegantTextHeight(boolean elegant) { mTextPaint.setElegantTextHeight(elegant); @@ -8194,7 +8197,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/TimePickerDelegate.java b/core/java/android/widget/TimePickerDelegate.java index 79256e5..ba93ee5 100644 --- a/core/java/android/widget/TimePickerDelegate.java +++ b/core/java/android/widget/TimePickerDelegate.java @@ -140,13 +140,12 @@ class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implement mSelectMinutes = res.getString(R.string.select_minutes); mHeaderSelectedColor = a.getColor(R.styleable.TimePicker_headerSelectedTextColor, - android.R.color.holo_blue_light); + R.color.timepicker_default_selector_color_quantum); - mHeaderUnSelectedColor = getUnselectedColor( - R.color.timepicker_default_text_color_holo_light); + mHeaderUnSelectedColor = getUnselectedColor(R.color.timepicker_default_text_color_quantum); if (mHeaderUnSelectedColor == -1) { mHeaderUnSelectedColor = a.getColor(R.styleable.TimePicker_headerUnselectedTextColor, - R.color.timepicker_default_text_color_holo_light); + R.color.timepicker_default_text_color_quantum); } final int headerBackgroundColor = a.getColor( @@ -296,7 +295,7 @@ class TimePickerDelegate extends TimePicker.AbstractTimePickerDelegate implement TypedArray a = mContext.obtainStyledAttributes(TEXT_APPEARANCE_TIME_LABEL_ATTR); final int textAppearanceResId = a.getResourceId(0, 0); tempView.setTextAppearance(mContext, (textAppearanceResId != 0) ? - textAppearanceResId : R.style.TextAppearance_Holo_TimePicker_TimeLabel); + textAppearanceResId : R.style.TextAppearance_Quantum_TimePicker_TimeLabel); a.recycle(); ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); diff --git a/core/java/android/widget/Toolbar.java b/core/java/android/widget/Toolbar.java index 075feba..cbd9a6a 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; } @@ -800,7 +1339,8 @@ public class Toolbar extends ViewGroup { return getPaddingTop(); case Gravity.BOTTOM: - return getPaddingBottom() - child.getMeasuredHeight() - lp.bottomMargin; + return getHeight() - getPaddingBottom() - + child.getMeasuredHeight() - lp.bottomMargin; default: case Gravity.CENTER_VERTICAL: @@ -853,17 +1393,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 +1439,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 +1457,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 +1470,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 +1509,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 +1537,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 +1580,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) { |
