diff options
Diffstat (limited to 'core/java/com')
18 files changed, 6588 insertions, 585 deletions
diff --git a/core/java/com/android/internal/app/AlertController.java b/core/java/com/android/internal/app/AlertController.java index b8110e3..61ee00c 100644 --- a/core/java/com/android/internal/app/AlertController.java +++ b/core/java/com/android/internal/app/AlertController.java @@ -526,11 +526,15 @@ public class AlertController { mWindow.setCloseOnTouchOutsideIfNotSet(true); } - // Only display the divider if we have a title and a custom view or a - // message. if (hasTopPanel) { + // Only clip scrolling content to padding if we have a title. + if (mScrollView != null) { + mScrollView.setClipToPadding(true); + } + + // Only show the divider if we have a title. final View divider; - if (mMessage != null || hasCustomPanel || mListView != null) { + if (mMessage != null || mListView != null || hasCustomPanel) { divider = topPanel.findViewById(R.id.titleDivider); } else { divider = topPanel.findViewById(R.id.titleDividerTop); @@ -541,6 +545,17 @@ public class AlertController { } } + // Update scroll indicators as needed. + if (!hasCustomPanel) { + final View content = mListView != null ? mListView : mScrollView; + if (content != null) { + final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0) + | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0); + content.setScrollIndicators(indicators, + View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM); + } + } + final TypedArray a = mContext.obtainStyledAttributes( null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0); setBackground(a, topPanel, contentPanel, customPanel, buttonPanel, @@ -654,59 +669,6 @@ public class AlertController { contentPanel.setVisibility(View.GONE); } } - - // Set up scroll indicators (if present). - final View indicatorUp = contentPanel.findViewById(R.id.scrollIndicatorUp); - final View indicatorDown = contentPanel.findViewById(R.id.scrollIndicatorDown); - if (indicatorUp != null || indicatorDown != null) { - if (mMessage != null) { - // We're just showing the ScrollView, set up listener. - mScrollView.setOnScrollChangeListener(new View.OnScrollChangeListener() { - @Override - public void onScrollChange(View v, int scrollX, int scrollY, - int oldScrollX, int oldScrollY) { - manageScrollIndicators(v, indicatorUp, indicatorDown); - } - }); - // Set up the indicators following layout. - mScrollView.post(new Runnable() { - @Override - public void run() { - manageScrollIndicators(mScrollView, indicatorUp, indicatorDown); - } - }); - - } else if (mListView != null) { - // We're just showing the AbsListView, set up listener. - mListView.setOnScrollListener(new AbsListView.OnScrollListener() { - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - // That's cool, I guess? - } - - @Override - public void onScroll(AbsListView v, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { - manageScrollIndicators(v, indicatorUp, indicatorDown); - } - }); - // Set up the indicators following layout. - mListView.post(new Runnable() { - @Override - public void run() { - manageScrollIndicators(mListView, indicatorUp, indicatorDown); - } - }); - } else { - // We don't have any content to scroll, remove the indicators. - if (indicatorUp != null) { - contentPanel.removeView(indicatorUp); - } - if (indicatorDown != null) { - contentPanel.removeView(indicatorDown); - } - } - } } private static void manageScrollIndicators(View v, View upIndicator, View downIndicator) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index e347faa..62ca1f0 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -21,6 +21,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; import android.content.ServiceConnection; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; @@ -34,6 +35,7 @@ import android.os.IBinder; import android.os.Message; import android.os.Parcelable; import android.os.RemoteException; +import android.os.ResultReceiver; import android.os.UserHandle; import android.service.chooser.ChooserTarget; import android.service.chooser.ChooserTargetService; @@ -53,11 +55,13 @@ public class ChooserActivity extends ResolverActivity { private static final boolean DEBUG = false; - private static final int QUERY_TARGET_LIMIT = 5; + private static final int QUERY_TARGET_SERVICE_LIMIT = 5; private static final int WATCHDOG_TIMEOUT_MILLIS = 5000; private Bundle mReplacementExtras; private IntentSender mChosenComponentSender; + private IntentSender mRefinementIntentSender; + private RefinementResultReceiver mRefinementResultReceiver; private ChooserTarget[] mCallerChooserTargets; @@ -113,6 +117,32 @@ public class ChooserActivity extends ResolverActivity { if (target != null) { modifyTargetIntent(target); } + Parcelable[] targetsParcelable + = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS); + if (targetsParcelable != null) { + final boolean offset = target == null; + Intent[] additionalTargets = + new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length]; + for (int i = 0; i < targetsParcelable.length; i++) { + if (!(targetsParcelable[i] instanceof Intent)) { + Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: " + + targetsParcelable[i]); + finish(); + super.onCreate(null); + return; + } + final Intent additionalTarget = (Intent) targetsParcelable[i]; + if (i == 0 && target == null) { + target = additionalTarget; + modifyTargetIntent(target); + } else { + additionalTargets[offset ? i - 1 : i] = additionalTarget; + modifyTargetIntent(additionalTarget); + } + } + setAdditionalTargets(additionalTargets); + } + mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS); CharSequence title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE); int defaultTitleRes = 0; @@ -125,7 +155,7 @@ public class ChooserActivity extends ResolverActivity { initialIntents = new Intent[pa.length]; for (int i=0; i<pa.length; i++) { if (!(pa[i] instanceof Intent)) { - Log.w("ChooserActivity", "Initial intent #" + i + " not an Intent: " + pa[i]); + Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]); finish(); super.onCreate(null); return; @@ -141,8 +171,7 @@ public class ChooserActivity extends ResolverActivity { final ChooserTarget[] targets = new ChooserTarget[pa.length]; for (int i = 0; i < pa.length; i++) { if (!(pa[i] instanceof ChooserTarget)) { - Log.w("ChooserActivity", "Chooser target #" + i + " is not a ChooserTarget: " + - pa[i]); + Log.w(TAG, "Chooser target #" + i + " is not a ChooserTarget: " + pa[i]); finish(); super.onCreate(null); return; @@ -153,12 +182,23 @@ public class ChooserActivity extends ResolverActivity { } mChosenComponentSender = intent.getParcelableExtra( Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER); + mRefinementIntentSender = intent.getParcelableExtra( + Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); setSafeForwardingMode(true); super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents, null, false); } @Override + protected void onDestroy() { + super.onDestroy(); + if (mRefinementResultReceiver != null) { + mRefinementResultReceiver.destroy(); + mRefinementResultReceiver = null; + } + } + + @Override public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { Intent result = defIntent; if (mReplacementExtras != null) { @@ -211,6 +251,37 @@ public class ChooserActivity extends ResolverActivity { } } + @Override + protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { + if (mRefinementIntentSender != null) { + final Intent fillIn = new Intent(); + final List<Intent> sourceIntents = target.getAllSourceIntents(); + if (!sourceIntents.isEmpty()) { + fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); + if (sourceIntents.size() > 1) { + final Intent[] alts = new Intent[sourceIntents.size() - 1]; + for (int i = 1, N = sourceIntents.size(); i < N; i++) { + alts[i - 1] = sourceIntents.get(i); + } + fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts); + } + if (mRefinementResultReceiver != null) { + mRefinementResultReceiver.destroy(); + } + mRefinementResultReceiver = new RefinementResultReceiver(this, target, null); + fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, + mRefinementResultReceiver); + try { + mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null); + return false; + } catch (SendIntentException e) { + Log.e(TAG, "Refinement IntentSender failed to send", e); + } + } + } + return super.onTargetSelected(target, alwaysCheck); + } + void queryTargetServices(ChooserListAdapter adapter) { final PackageManager pm = getPackageManager(); int targetsToQuery = 0; @@ -258,8 +329,9 @@ public class ChooserActivity extends ResolverActivity { targetsToQuery++; } } - if (targetsToQuery >= QUERY_TARGET_LIMIT) { - if (DEBUG) Log.d(TAG, "queryTargets hit query target limit " + QUERY_TARGET_LIMIT); + if (targetsToQuery >= QUERY_TARGET_SERVICE_LIMIT) { + if (DEBUG) Log.d(TAG, "queryTargets hit query target limit " + + QUERY_TARGET_SERVICE_LIMIT); break; } } @@ -303,6 +375,43 @@ public class ChooserActivity extends ResolverActivity { mTargetResultHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); } + void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) { + if (mRefinementResultReceiver != null) { + mRefinementResultReceiver.destroy(); + mRefinementResultReceiver = null; + } + + if (selectedTarget == null) { + Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); + } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) { + Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget + + " cannot match refined source intent " + matchingIntent); + } else if (super.onTargetSelected(selectedTarget.cloneFilledIn(matchingIntent, 0), false)) { + finish(); + return; + } + onRefinementCanceled(); + } + + void onRefinementCanceled() { + if (mRefinementResultReceiver != null) { + mRefinementResultReceiver.destroy(); + mRefinementResultReceiver = null; + } + finish(); + } + + boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) { + final List<Intent> targetIntents = target.getAllSourceIntents(); + for (int i = 0, N = targetIntents.size(); i < N; i++) { + final Intent targetIntent = targetIntents.get(i); + if (targetIntent.filterEquals(matchingIntent)) { + return true; + } + } + return false; + } + @Override ResolveListAdapter createAdapter(Context context, Intent[] initialIntents, List<ResolveInfo> rList, int launchedFromUid, boolean filterLastUsed) { @@ -313,17 +422,19 @@ public class ChooserActivity extends ResolverActivity { return adapter; } - class ChooserTargetInfo implements TargetInfo { - private final TargetInfo mSourceInfo; + final class ChooserTargetInfo implements TargetInfo { + private final DisplayResolveInfo mSourceInfo; private final ResolveInfo mBackupResolveInfo; private final ChooserTarget mChooserTarget; private final Drawable mDisplayIcon; + private final Intent mFillInIntent; + private final int mFillInFlags; public ChooserTargetInfo(ChooserTarget target) { this(null, target); } - public ChooserTargetInfo(TargetInfo sourceInfo, ChooserTarget chooserTarget) { + public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget) { mSourceInfo = sourceInfo; mChooserTarget = chooserTarget; mDisplayIcon = new BitmapDrawable(getResources(), chooserTarget.getIcon()); @@ -333,6 +444,18 @@ public class ChooserActivity extends ResolverActivity { } else { mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0); } + + mFillInIntent = null; + mFillInFlags = 0; + } + + private ChooserTargetInfo(ChooserTargetInfo other, Intent fillInIntent, int flags) { + mSourceInfo = other.mSourceInfo; + mBackupResolveInfo = other.mBackupResolveInfo; + mChooserTarget = other.mChooserTarget; + mDisplayIcon = other.mDisplayIcon; + mFillInIntent = fillInIntent; + mFillInFlags = flags; } @Override @@ -358,22 +481,42 @@ public class ChooserActivity extends ResolverActivity { } private Intent getFillInIntent() { - return mSourceInfo != null ? mSourceInfo.getResolvedIntent() : getTargetIntent(); + Intent result = mSourceInfo != null + ? mSourceInfo.getResolvedIntent() : getTargetIntent(); + if (result == null) { + Log.e(TAG, "ChooserTargetInfo#getFillInIntent: no fillIn intent available"); + } else if (mFillInIntent != null) { + result = new Intent(result); + result.fillIn(mFillInIntent, mFillInFlags); + } + return result; } @Override public boolean start(Activity activity, Bundle options) { - return mChooserTarget.sendIntent(activity, getFillInIntent()); + final Intent intent = getFillInIntent(); + if (intent == null) { + return false; + } + return mChooserTarget.sendIntent(activity, intent); } @Override public boolean startAsCaller(Activity activity, Bundle options, int userId) { - return mChooserTarget.sendIntentAsCaller(activity, getFillInIntent(), userId); + final Intent intent = getFillInIntent(); + if (intent == null) { + return false; + } + return mChooserTarget.sendIntentAsCaller(activity, intent, userId); } @Override public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - return mChooserTarget.sendIntentAsUser(activity, getFillInIntent(), user); + final Intent intent = getFillInIntent(); + if (intent == null) { + return false; + } + return mChooserTarget.sendIntentAsUser(activity, intent, user); } @Override @@ -395,6 +538,21 @@ public class ChooserActivity extends ResolverActivity { public Drawable getDisplayIcon() { return mDisplayIcon; } + + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new ChooserTargetInfo(this, fillInIntent, flags); + } + + @Override + public List<Intent> getAllSourceIntents() { + final List<Intent> results = new ArrayList<>(); + if (mSourceInfo != null) { + // We only queried the service for the first one in our sourceinfo. + results.add(mSourceInfo.getAllSourceIntents().get(0)); + } + return results; + } } public class ChooserListAdapter extends ResolveListAdapter { @@ -542,4 +700,53 @@ public class ChooserActivity extends ResolverActivity { connection = c; } } + + static class RefinementResultReceiver extends ResultReceiver { + private ChooserActivity mChooserActivity; + private TargetInfo mSelectedTarget; + + public RefinementResultReceiver(ChooserActivity host, TargetInfo target, + Handler handler) { + super(handler); + mChooserActivity = host; + mSelectedTarget = target; + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (mChooserActivity == null) { + Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); + return; + } + if (resultData == null) { + Log.e(TAG, "RefinementResultReceiver received null resultData"); + return; + } + + switch (resultCode) { + case RESULT_CANCELED: + mChooserActivity.onRefinementCanceled(); + break; + case RESULT_OK: + Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); + if (intentParcelable instanceof Intent) { + mChooserActivity.onRefinementResult(mSelectedTarget, + (Intent) intentParcelable); + } else { + Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent" + + " in resultData with key Intent.EXTRA_INTENT"); + } + break; + default: + Log.w(TAG, "Unknown result code " + resultCode + + " sent to RefinementResultReceiver"); + break; + } + } + + public void destroy() { + mChooserActivity = null; + mSelectedTarget = null; + } + } } diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl index 4c6db24..644adb6 100644 --- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl +++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl @@ -97,6 +97,12 @@ interface IVoiceInteractionManagerService { void showSessionForActiveService(IVoiceInteractionSessionShowCallback showCallback); /** + * Notifies the active service that a launch was requested from the Keyguard. This will only + * be called if {@link #activeServiceSupportsLaunchFromKeyguard()} returns true. + */ + void launchVoiceAssistFromKeyguard(); + + /** * Indicates whether there is a voice session running (but not necessarily showing). */ boolean isSessionRunning(); @@ -106,4 +112,10 @@ interface IVoiceInteractionManagerService { * assist gesture. */ boolean activeServiceSupportsAssist(); + + /** + * Indicates whether the currently active voice interaction service is capable of being launched + * from the lockscreen. + */ + boolean activeServiceSupportsLaunchFromKeyguard(); } diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index 8dd7836..2048664 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -102,7 +102,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic private int mLastSelected = AbsListView.INVALID_POSITION; private boolean mResolvingHome = false; private int mProfileSwitchMessageId = -1; - private Intent mIntent; + private final ArrayList<Intent> mIntents = new ArrayList<>(); private UsageStatsManager mUsm; private Map<String, UsageStats> mStats; @@ -229,7 +229,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic final ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE); mIconDpi = am.getLauncherLargeIconDensity(); - mIntent = new Intent(intent); + mIntents.add(0, new Intent(intent)); mAdapter = createAdapter(this, initialIntents, rList, mLaunchedFromUid, alwaysUseOption); final int layoutId; @@ -250,7 +250,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return; } - int count = mAdapter.mList.size(); + int count = mAdapter.mDisplayList.size(); if (count > 1 || (count == 1 && mAdapter.getOtherProfile() != null)) { setContentView(layoutId); mAdapterView = (AbsListView) findViewById(R.id.resolver_list); @@ -376,8 +376,16 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } } + protected final void setAdditionalTargets(Intent[] intents) { + if (intents != null) { + for (Intent intent : intents) { + mIntents.add(intent); + } + } + } + public Intent getTargetIntent() { - return mIntent; + return mIntents.isEmpty() ? null : mIntents.get(0); } private String getReferrerPackageName() { @@ -630,8 +638,9 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } TargetInfo target = mAdapter.targetInfoForPosition(which, filtered); - onTargetSelected(target, always); - finish(); + if (onTargetSelected(target, always)) { + finish(); + } } /** @@ -641,7 +650,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return defIntent; } - protected void onTargetSelected(TargetInfo target, boolean alwaysCheck) { + protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { final ResolveInfo ri = target.getResolveInfo(); final Intent intent = target != null ? target.getResolvedIntent() : null; @@ -728,7 +737,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic ComponentName[] set = new ComponentName[N]; int bestMatch = 0; for (int i=0; i<N; i++) { - ResolveInfo r = mAdapter.mOrigResolveList.get(i); + ResolveInfo r = mAdapter.mOrigResolveList.get(i).getResolveInfoAt(0); set[i] = new ComponentName(r.activityInfo.packageName, r.activityInfo.name); if (r.match > bestMatch) bestMatch = r.match; @@ -774,6 +783,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (target != null) { safelyStartActivity(target); } + return true; } void safelyStartActivity(TargetInfo cti) { @@ -837,15 +847,17 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic private Drawable mDisplayIcon; private final CharSequence mExtendedInfo; private final Intent mResolvedIntent; + private final List<Intent> mSourceIntents = new ArrayList<>(); - DisplayResolveInfo(ResolveInfo pri, CharSequence pLabel, + DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) { + mSourceIntents.add(originalIntent); mResolveInfo = pri; mDisplayLabel = pLabel; mExtendedInfo = pInfo; final Intent intent = new Intent(pOrigIntent != null ? pOrigIntent : - getReplacementIntent(pri.activityInfo, mIntent)); + getReplacementIntent(pri.activityInfo, getTargetIntent())); intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); final ActivityInfo ai = mResolveInfo.activityInfo; @@ -854,6 +866,16 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic mResolvedIntent = intent; } + private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags) { + mSourceIntents.addAll(other.getAllSourceIntents()); + mResolveInfo = other.mResolveInfo; + mDisplayLabel = other.mDisplayLabel; + mDisplayIcon = other.mDisplayIcon; + mExtendedInfo = other.mExtendedInfo; + mResolvedIntent = new Intent(other.mResolvedIntent); + mResolvedIntent.fillIn(fillInIntent, flags); + } + public ResolveInfo getResolveInfo() { return mResolveInfo; } @@ -866,6 +888,20 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return mDisplayIcon; } + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new DisplayResolveInfo(this, fillInIntent, flags); + } + + @Override + public List<Intent> getAllSourceIntents() { + return mSourceIntents; + } + + public void addAlternateSourceIntent(Intent alt) { + mSourceIntents.add(alt); + } + public void setDisplayIcon(Drawable icon) { mDisplayIcon = icon; } @@ -986,6 +1022,16 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic * @return The drawable that should be used to represent this target */ public Drawable getDisplayIcon(); + + /** + * Clone this target with the given fill-in information. + */ + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags); + + /** + * @return the list of supported source intents deduped against this single target + */ + public List<Intent> getAllSourceIntents(); } class ResolveListAdapter extends BaseAdapter { @@ -998,8 +1044,8 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic protected final LayoutInflater mInflater; - List<DisplayResolveInfo> mList; - List<ResolveInfo> mOrigResolveList; + List<DisplayResolveInfo> mDisplayList; + List<ResolvedComponentInfo> mOrigResolveList; private int mLastChosenPosition = -1; private boolean mFilterLastUsed; @@ -1010,7 +1056,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic mBaseResolveList = rList; mLaunchedFromUid = launchedFromUid; mInflater = LayoutInflater.from(context); - mList = new ArrayList<>(); + mDisplayList = new ArrayList<>(); mFilterLastUsed = filterLastUsed; rebuildList(); } @@ -1027,7 +1073,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic public DisplayResolveInfo getFilteredItem() { if (mFilterLastUsed && mLastChosenPosition >= 0) { // Not using getItem since it offsets to dodge this position for the list - return mList.get(mLastChosenPosition); + return mDisplayList.get(mLastChosenPosition); } return null; } @@ -1048,11 +1094,12 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } private void rebuildList() { - List<ResolveInfo> currentResolveList; + List<ResolvedComponentInfo> currentResolveList = null; try { + final Intent primaryIntent = getTargetIntent(); mLastChosen = AppGlobals.getPackageManager().getLastChosenActivity( - mIntent, mIntent.resolveTypeIfNeeded(getContentResolver()), + primaryIntent, primaryIntent.resolveTypeIfNeeded(getContentResolver()), PackageManager.MATCH_DEFAULT_ONLY); } catch (RemoteException re) { Log.d(TAG, "Error calling setLastChosenActivity\n" + re); @@ -1060,15 +1107,27 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic // Clear the value of mOtherProfile from previous call. mOtherProfile = null; - mList.clear(); + mDisplayList.clear(); if (mBaseResolveList != null) { - currentResolveList = mOrigResolveList = mBaseResolveList; + currentResolveList = mOrigResolveList = new ArrayList<>(); + addResolveListDedupe(currentResolveList, getTargetIntent(), mBaseResolveList); } else { - currentResolveList = mOrigResolveList = mPm.queryIntentActivities(mIntent, - PackageManager.MATCH_DEFAULT_ONLY - | (shouldGetResolvedFilter() ? PackageManager.GET_RESOLVED_FILTER : 0) - | (shouldGetActivityMetadata() ? PackageManager.GET_META_DATA : 0) - ); + final boolean shouldGetResolvedFilter = shouldGetResolvedFilter(); + final boolean shouldGetActivityMetadata = shouldGetActivityMetadata(); + for (int i = 0, N = mIntents.size(); i < N; i++) { + final Intent intent = mIntents.get(i); + final List<ResolveInfo> infos = mPm.queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY + | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0) + | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0)); + if (infos != null) { + if (currentResolveList == null) { + currentResolveList = mOrigResolveList = new ArrayList<>(); + } + addResolveListDedupe(currentResolveList, intent, infos); + } + } + // Filter out any activities that the launched uid does not // have permission for. We don't do this when we have an explicit // list of resolved activities, because that only happens when @@ -1076,14 +1135,15 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic // they gave us. if (currentResolveList != null) { for (int i=currentResolveList.size()-1; i >= 0; i--) { - ActivityInfo ai = currentResolveList.get(i).activityInfo; + ActivityInfo ai = currentResolveList.get(i) + .getResolveInfoAt(0).activityInfo; int granted = ActivityManager.checkComponentPermission( ai.permission, mLaunchedFromUid, ai.applicationInfo.uid, ai.exported); if (granted != PackageManager.PERMISSION_GRANTED) { // Access not allowed! if (mOrigResolveList == currentResolveList) { - mOrigResolveList = new ArrayList<ResolveInfo>(mOrigResolveList); + mOrigResolveList = new ArrayList<>(mOrigResolveList); } currentResolveList.remove(i); } @@ -1094,9 +1154,10 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if ((currentResolveList != null) && ((N = currentResolveList.size()) > 0)) { // Only display the first matches that are either of equal // priority or have asked to be default options. - ResolveInfo r0 = currentResolveList.get(0); + ResolvedComponentInfo rci0 = currentResolveList.get(0); + ResolveInfo r0 = rci0.getResolveInfoAt(0); for (int i=1; i<N; i++) { - ResolveInfo ri = currentResolveList.get(i); + ResolveInfo ri = currentResolveList.get(i).getResolveInfoAt(0); if (DEBUG) Log.v( TAG, r0.activityInfo.name + "=" + @@ -1107,7 +1168,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic r0.isDefault != ri.isDefault) { while (i < N) { if (mOrigResolveList == currentResolveList) { - mOrigResolveList = new ArrayList<ResolveInfo>(mOrigResolveList); + mOrigResolveList = new ArrayList<>(mOrigResolveList); } currentResolveList.remove(i); N--; @@ -1115,9 +1176,8 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } } if (N > 1) { - Comparator<ResolveInfo> rComparator = - new ResolverComparator(ResolverActivity.this, mIntent); - Collections.sort(currentResolveList, rComparator); + Collections.sort(currentResolveList, + new ResolverComparator(ResolverActivity.this, getTargetIntent())); } // First put the initial items at the top. if (mInitialIntents != null) { @@ -1146,14 +1206,15 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic ri.nonLocalizedLabel = li.getNonLocalizedLabel(); ri.icon = li.getIconResource(); } - addResolveInfo(new DisplayResolveInfo(ri, + addResolveInfo(new DisplayResolveInfo(ii, ri, ri.loadLabel(getPackageManager()), null, ii)); } } // Check for applications with same name and use application name or // package name if necessary - r0 = currentResolveList.get(0); + rci0 = currentResolveList.get(0); + r0 = rci0.getResolveInfoAt(0); int start = 0; CharSequence r0Label = r0.loadLabel(mPm); mHasExtendedInfo = false; @@ -1161,7 +1222,8 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (r0Label == null) { r0Label = r0.activityInfo.packageName; } - ResolveInfo ri = currentResolveList.get(i); + ResolvedComponentInfo rci = currentResolveList.get(i); + ResolveInfo ri = rci.getResolveInfoAt(0); CharSequence riLabel = ri.loadLabel(mPm); if (riLabel == null) { riLabel = ri.activityInfo.packageName; @@ -1169,13 +1231,14 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (riLabel.equals(r0Label)) { continue; } - processGroup(currentResolveList, start, (i-1), r0, r0Label); + processGroup(currentResolveList, start, (i-1), rci0, r0Label); + rci0 = rci; r0 = ri; r0Label = riLabel; start = i; } // Process last group - processGroup(currentResolveList, start, (N-1), r0, r0Label); + processGroup(currentResolveList, start, (N-1), rci0, r0Label); } // Layout doesn't handle both profile button and last chosen @@ -1188,6 +1251,36 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic onListRebuilt(); } + private void addResolveListDedupe(List<ResolvedComponentInfo> into, Intent intent, + List<ResolveInfo> from) { + final int fromCount = from.size(); + final int intoCount = into.size(); + for (int i = 0; i < fromCount; i++) { + final ResolveInfo newInfo = from.get(i); + boolean found = false; + // Only loop to the end of into as it was before we started; no dupes in from. + for (int j = 0; j < intoCount; j++) { + final ResolvedComponentInfo rci = into.get(i); + if (isSameResolvedComponent(newInfo, rci)) { + found = true; + rci.add(intent, newInfo); + break; + } + } + if (!found) { + into.add(new ResolvedComponentInfo(new ComponentName( + newInfo.activityInfo.packageName, newInfo.activityInfo.name), + intent, newInfo)); + } + } + } + + private boolean isSameResolvedComponent(ResolveInfo a, ResolvedComponentInfo b) { + final ActivityInfo ai = a.activityInfo; + return ai.packageName.equals(b.name.getPackageName()) + && ai.name.equals(b.name.getClassName()); + } + public void onListRebuilt() { // This space for rent } @@ -1196,18 +1289,18 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic return mFilterLastUsed; } - private void processGroup(List<ResolveInfo> rList, int start, int end, ResolveInfo ro, - CharSequence roLabel) { + private void processGroup(List<ResolvedComponentInfo> rList, int start, int end, + ResolvedComponentInfo ro, CharSequence roLabel) { // Process labels from start to i int num = end - start+1; if (num == 1) { // No duplicate labels. Use label for entry at start - addResolveInfo(new DisplayResolveInfo(ro, roLabel, null, null)); - updateLastChosenPosition(ro); + addResolveInfoWithAlternates(ro, null, roLabel); } else { mHasExtendedInfo = true; boolean usePkg = false; - CharSequence startApp = ro.activityInfo.applicationInfo.loadLabel(mPm); + CharSequence startApp = ro.getResolveInfoAt(0).activityInfo.applicationInfo + .loadLabel(mPm); if (startApp == null) { usePkg = true; } @@ -1217,7 +1310,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic new HashSet<CharSequence>(); duplicates.add(startApp); for (int j = start+1; j <= end ; j++) { - ResolveInfo jRi = rList.get(j); + ResolveInfo jRi = rList.get(j).getResolveInfoAt(0); CharSequence jApp = jRi.activityInfo.applicationInfo.loadLabel(mPm); if ( (jApp == null) || (duplicates.contains(jApp))) { usePkg = true; @@ -1230,26 +1323,46 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic duplicates.clear(); } for (int k = start; k <= end; k++) { - ResolveInfo add = rList.get(k); + final ResolvedComponentInfo rci = rList.get(k); + final ResolveInfo add = rci.getResolveInfoAt(0); + final CharSequence extraInfo; if (usePkg) { - // Use application name for all entries from start to end-1 - addResolveInfo(new DisplayResolveInfo(add, roLabel, - add.activityInfo.packageName, null)); - } else { // Use package name for all entries from start to end-1 - addResolveInfo(new DisplayResolveInfo(add, roLabel, - add.activityInfo.applicationInfo.loadLabel(mPm), null)); + extraInfo = add.activityInfo.packageName; + } else { + // Use application name for all entries from start to end-1 + extraInfo = add.activityInfo.applicationInfo.loadLabel(mPm); } - updateLastChosenPosition(add); + addResolveInfoWithAlternates(rci, extraInfo, roLabel); + } + } + } + + private void addResolveInfoWithAlternates(ResolvedComponentInfo rci, + CharSequence extraInfo, CharSequence roLabel) { + final int count = rci.getCount(); + final Intent intent = rci.getIntentAt(0); + final ResolveInfo add = rci.getResolveInfoAt(0); + final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent); + final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, roLabel, + extraInfo, replaceIntent); + addResolveInfo(dri); + if (replaceIntent == intent) { + // Only add alternates if we didn't get a specific replacement from + // the caller. If we have one it trumps potential alternates. + for (int i = 1, N = count; i < N; i++) { + final Intent altIntent = rci.getIntentAt(i); + dri.addAlternateSourceIntent(altIntent); } } + updateLastChosenPosition(add); } private void updateLastChosenPosition(ResolveInfo info) { if (mLastChosen != null && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { - mLastChosenPosition = mList.size() - 1; + mLastChosenPosition = mDisplayList.size() - 1; } } @@ -1259,20 +1372,21 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic // The first one we see gets special treatment. mOtherProfile = dri; } else { - mList.add(dri); + mDisplayList.add(dri); } } public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { - return (filtered ? getItem(position) : mList.get(position)).getResolveInfo(); + return (filtered ? getItem(position) : mDisplayList.get(position)) + .getResolveInfo(); } public TargetInfo targetInfoForPosition(int position, boolean filtered) { - return filtered ? getItem(position) : mList.get(position); + return filtered ? getItem(position) : mDisplayList.get(position); } public int getCount() { - int result = mList.size(); + int result = mDisplayList.size(); if (mFilterLastUsed && mLastChosenPosition >= 0) { result--; } @@ -1283,7 +1397,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { position++; } - return mList.get(position); + return mDisplayList.get(position); } public long getItemId(int position) { @@ -1295,8 +1409,8 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } public boolean hasResolvedTarget(ResolveInfo info) { - for (int i = 0, N = mList.size(); i < N; i++) { - if (info.equals(mList.get(i).getResolveInfo())) { + for (int i = 0, N = mDisplayList.size(); i < N; i++) { + if (info.equals(mDisplayList.get(i).getResolveInfo())) { return true; } } @@ -1304,11 +1418,12 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } protected int getDisplayResolveInfoCount() { - return mList.size(); + return mDisplayList.size(); } protected DisplayResolveInfo getDisplayResolveInfo(int index) { - return mList.get(index); + // Used to query services. We only query services for primary targets, not alternates. + return mDisplayList.get(index); } public final View getView(int position, View convertView, ViewGroup parent) { @@ -1349,6 +1464,52 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } } + static final class ResolvedComponentInfo { + public final ComponentName name; + private final List<Intent> mIntents = new ArrayList<>(); + private final List<ResolveInfo> mResolveInfos = new ArrayList<>(); + + public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) { + this.name = name; + add(intent, info); + } + + public void add(Intent intent, ResolveInfo info) { + mIntents.add(intent); + mResolveInfos.add(info); + } + + public int getCount() { + return mIntents.size(); + } + + public Intent getIntentAt(int index) { + return index >= 0 ? mIntents.get(index) : null; + } + + public ResolveInfo getResolveInfoAt(int index) { + return index >= 0 ? mResolveInfos.get(index) : null; + } + + public int findIntent(Intent intent) { + for (int i = 0, N = mIntents.size(); i < N; i++) { + if (intent.equals(mIntents.get(i))) { + return i; + } + } + return -1; + } + + public int findResolveInfo(ResolveInfo info) { + for (int i = 0, N = mResolveInfos.size(); i < N; i++) { + if (info.equals(mResolveInfos.get(i))) { + return i; + } + } + return -1; + } + } + static class ViewHolder { public TextView text; public TextView text2; @@ -1435,7 +1596,7 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic && match <= IntentFilter.MATCH_CATEGORY_PATH; } - class ResolverComparator implements Comparator<ResolveInfo> { + class ResolverComparator implements Comparator<ResolvedComponentInfo> { private final Collator mCollator; private final boolean mHttp; @@ -1446,7 +1607,10 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic } @Override - public int compare(ResolveInfo lhs, ResolveInfo rhs) { + public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) { + final ResolveInfo lhs = lhsp.getResolveInfoAt(0); + final ResolveInfo rhs = rhsp.getResolveInfoAt(0); + // We want to put the one targeted to another user at the end of the dialog. if (lhs.targetUserId != UserHandle.USER_CURRENT) { return 1; @@ -1487,7 +1651,6 @@ public class ResolverActivity extends Activity implements AdapterView.OnItemClic if (stats != null) { return stats.getTotalTimeInForeground(); } - } return 0; } diff --git a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java index 52485dd..ce94727 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/core/java/com/android/internal/inputmethod/InputMethodSubtypeSwitchingController.java @@ -196,7 +196,7 @@ public class InputMethodSubtypeSwitchingController { } public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( - boolean showSubtypes, boolean inputShown, boolean isScreenLocked) { + boolean showSubtypes, boolean includeAuxiliarySubtypes, boolean isScreenLocked) { final ArrayList<ImeSubtypeListItem> imList = new ArrayList<ImeSubtypeListItem>(); final HashMap<InputMethodInfo, List<InputMethodSubtype>> immis = @@ -205,6 +205,12 @@ public class InputMethodSubtypeSwitchingController { if (immis == null || immis.size() == 0) { return Collections.emptyList(); } + if (isScreenLocked && includeAuxiliarySubtypes) { + if (DEBUG) { + Slog.w(TAG, "Auxiliary subtypes are not allowed to be shown in lock screen."); + } + includeAuxiliarySubtypes = false; + } mSortedImmis.clear(); mSortedImmis.putAll(immis); for (InputMethodInfo imi : mSortedImmis.keySet()) { @@ -227,7 +233,7 @@ public class InputMethodSubtypeSwitchingController { final String subtypeHashCode = String.valueOf(subtype.hashCode()); // We show all enabled IMEs and subtypes when an IME is shown. if (enabledSubtypeSet.contains(subtypeHashCode) - && ((inputShown && !isScreenLocked) || !subtype.isAuxiliary())) { + && (includeAuxiliarySubtypes || !subtype.isAuxiliary())) { final CharSequence subtypeLabel = subtype.overridesImplicitlyEnabledSubtype() ? null : subtype .getDisplayName(mContext, imi.getPackageName(), @@ -516,8 +522,8 @@ public class InputMethodSubtypeSwitchingController { } public List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeListLocked(boolean showSubtypes, - boolean inputShown, boolean isScreenLocked) { + boolean includingAuxiliarySubtypes, boolean isScreenLocked) { return mSubtypeList.getSortedInputMethodAndSubtypeList( - showSubtypes, inputShown, isScreenLocked); + showSubtypes, includingAuxiliarySubtypes, isScreenLocked); } } diff --git a/core/java/com/android/internal/os/BinderInternal.java b/core/java/com/android/internal/os/BinderInternal.java index 240d9df..d77b998 100644 --- a/core/java/com/android/internal/os/BinderInternal.java +++ b/core/java/com/android/internal/os/BinderInternal.java @@ -20,6 +20,8 @@ import android.os.IBinder; import android.os.SystemClock; import android.util.EventLog; +import dalvik.system.VMRuntime; + import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -96,7 +98,7 @@ public class BinderInternal { public static void forceGc(String reason) { EventLog.writeEvent(2741, reason); - Runtime.getRuntime().gc(); + VMRuntime.getRuntime().requestConcurrentGC(); } static void forceBinderGc() { diff --git a/core/java/com/android/internal/os/MobileRadioPowerCalculator.java b/core/java/com/android/internal/os/MobileRadioPowerCalculator.java index 9711c3b..8586d76 100644 --- a/core/java/com/android/internal/os/MobileRadioPowerCalculator.java +++ b/core/java/com/android/internal/os/MobileRadioPowerCalculator.java @@ -52,7 +52,7 @@ public class MobileRadioPowerCalculator extends PowerCalculator { public MobileRadioPowerCalculator(PowerProfile profile, BatteryStats stats) { mPowerRadioOn = profile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE); for (int i = 0; i < mPowerBins.length; i++) { - mPowerBins[i] = profile.getAveragePower(PowerProfile.POWER_RADIO_ACTIVE, i); + mPowerBins[i] = profile.getAveragePower(PowerProfile.POWER_RADIO_ON, i); } mPowerScan = profile.getAveragePower(PowerProfile.POWER_RADIO_SCANNING); mStats = stats; @@ -128,7 +128,9 @@ public class MobileRadioPowerCalculator extends PowerCalculator { } if (power != 0) { - app.noCoveragePercent = noCoverageTimeMs * 100.0 / signalTimeMs; + if (signalTimeMs != 0) { + app.noCoveragePercent = noCoverageTimeMs * 100.0 / signalTimeMs; + } app.mobileActive = remainingActiveTimeMs; app.mobileActiveCount = stats.getMobileRadioActiveUnknownCount(statsType); app.mobileRadioPowerMah = power; diff --git a/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java new file mode 100644 index 0000000..2cb9c25 --- /dev/null +++ b/core/java/com/android/internal/policy/PhoneFallbackEventHandler.java @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.policy; + +import android.app.KeyguardManager; +import android.app.SearchManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.media.AudioManager; +import android.media.session.MediaSessionLegacyHelper; +import android.os.UserHandle; +import android.provider.Settings; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.view.FallbackEventHandler; +import android.view.HapticFeedbackConstants; +import android.view.KeyEvent; +import android.view.View; +import com.android.internal.policy.PhoneWindow; + +/** + * @hide + */ +public class PhoneFallbackEventHandler implements FallbackEventHandler { + private static String TAG = "PhoneFallbackEventHandler"; + private static final boolean DEBUG = false; + + Context mContext; + View mView; + + AudioManager mAudioManager; + KeyguardManager mKeyguardManager; + SearchManager mSearchManager; + TelephonyManager mTelephonyManager; + + public PhoneFallbackEventHandler(Context context) { + mContext = context; + } + + public void setView(View v) { + mView = v; + } + + public void preDispatchKeyEvent(KeyEvent event) { + getAudioManager().preDispatchKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE); + } + + public boolean dispatchKeyEvent(KeyEvent event) { + + final int action = event.getAction(); + final int keyCode = event.getKeyCode(); + + if (action == KeyEvent.ACTION_DOWN) { + return onKeyDown(keyCode, event); + } else { + return onKeyUp(keyCode, event); + } + } + + boolean onKeyDown(int keyCode, KeyEvent event) { + /* **************************************************************************** + * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES. + * See the comment in PhoneWindow.onKeyDown + * ****************************************************************************/ + final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState(); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + return true; + } + + + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + /* Suppress PLAY/PAUSE toggle when phone is ringing or in-call + * to avoid music playback */ + if (getTelephonyManager().getCallState() != TelephonyManager.CALL_STATE_IDLE) { + return true; // suppress key event + } + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + dispatcher.performedLongPress(event); + if (isUserSetupComplete()) { + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + // launch the VoiceDialer + Intent intent = new Intent(Intent.ACTION_VOICE_COMMAND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + sendCloseSystemWindows(); + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + startCallActivity(); + } + } else { + Log.i(TAG, "Not starting call activity because user " + + "setup is in progress."); + } + } + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + dispatcher.performedLongPress(event); + if (isUserSetupComplete()) { + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + sendCloseSystemWindows(); + // Broadcast an intent that the Camera button was longpressed + Intent intent = new Intent(Intent.ACTION_CAMERA_BUTTON, null); + intent.putExtra(Intent.EXTRA_KEY_EVENT, event); + mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT_OR_SELF, + null, null, null, 0, null, null); + } else { + Log.i(TAG, "Not dispatching CAMERA long press because user " + + "setup is in progress."); + } + } + return true; + } + + case KeyEvent.KEYCODE_SEARCH: { + if (getKeyguardManager().inKeyguardRestrictedInputMode() || dispatcher == null) { + break; + } + if (event.getRepeatCount() == 0) { + dispatcher.startTracking(event, this); + } else if (event.isLongPress() && dispatcher.isTracking(event)) { + Configuration config = mContext.getResources().getConfiguration(); + if (config.keyboard == Configuration.KEYBOARD_NOKEYS + || config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_YES) { + if (isUserSetupComplete()) { + // launch the search activity + Intent intent = new Intent(Intent.ACTION_SEARCH_LONG_PRESS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + sendCloseSystemWindows(); + getSearchManager().stopSearch(); + mContext.startActivity(intent); + // Only clear this if we successfully start the + // activity; otherwise we will allow the normal short + // press action to be performed. + dispatcher.performedLongPress(event); + return true; + } catch (ActivityNotFoundException e) { + // Ignore + } + } else { + Log.i(TAG, "Not dispatching SEARCH long press because user " + + "setup is in progress."); + } + } + } + break; + } + } + return false; + } + + boolean onKeyUp(int keyCode, KeyEvent event) { + if (DEBUG) { + Log.d(TAG, "up " + keyCode); + } + final KeyEvent.DispatcherState dispatcher = mView.getKeyDispatcherState(); + if (dispatcher != null) { + dispatcher.handleUpEvent(event); + } + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + if (!event.isCanceled()) { + MediaSessionLegacyHelper.getHelper(mContext).sendVolumeKeyEvent(event, false); + } + return true; + } + + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: { + handleMediaKeyEvent(event); + return true; + } + + case KeyEvent.KEYCODE_CAMERA: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + // Add short press behavior here if desired + } + return true; + } + + case KeyEvent.KEYCODE_CALL: { + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + if (isUserSetupComplete()) { + startCallActivity(); + } else { + Log.i(TAG, "Not starting call activity because user " + + "setup is in progress."); + } + } + return true; + } + } + return false; + } + + void startCallActivity() { + sendCloseSystemWindows(); + Intent intent = new Intent(Intent.ACTION_CALL_BUTTON); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + mContext.startActivity(intent); + } catch (ActivityNotFoundException e) { + Log.w(TAG, "No activity found for android.intent.action.CALL_BUTTON."); + } + } + + SearchManager getSearchManager() { + if (mSearchManager == null) { + mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE); + } + return mSearchManager; + } + + TelephonyManager getTelephonyManager() { + if (mTelephonyManager == null) { + mTelephonyManager = (TelephonyManager)mContext.getSystemService( + Context.TELEPHONY_SERVICE); + } + return mTelephonyManager; + } + + KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager)mContext.getSystemService(Context.KEYGUARD_SERVICE); + } + return mKeyguardManager; + } + + AudioManager getAudioManager() { + if (mAudioManager == null) { + mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); + } + return mAudioManager; + } + + void sendCloseSystemWindows() { + PhoneWindow.sendCloseSystemWindows(mContext, null); + } + + private void handleMediaKeyEvent(KeyEvent keyEvent) { + MediaSessionLegacyHelper.getHelper(mContext).sendMediaButtonEvent(keyEvent, false); + } + + private boolean isUserSetupComplete() { + return Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.USER_SETUP_COMPLETE, 0) != 0; + } +} + diff --git a/core/java/com/android/internal/policy/PhoneLayoutInflater.java b/core/java/com/android/internal/policy/PhoneLayoutInflater.java new file mode 100644 index 0000000..991b6bb --- /dev/null +++ b/core/java/com/android/internal/policy/PhoneLayoutInflater.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.policy; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +/** + * @hide + */ +public class PhoneLayoutInflater extends LayoutInflater { + private static final String[] sClassPrefixList = { + "android.widget.", + "android.webkit.", + "android.app." + }; + + /** + * Instead of instantiating directly, you should retrieve an instance + * through {@link Context#getSystemService} + * + * @param context The Context in which in which to find resources and other + * application-specific things. + * + * @see Context#getSystemService + */ + public PhoneLayoutInflater(Context context) { + super(context); + } + + protected PhoneLayoutInflater(LayoutInflater original, Context newContext) { + super(original, newContext); + } + + /** Override onCreateView to instantiate names that correspond to the + widgets known to the Widget factory. If we don't find a match, + call through to our super class. + */ + @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException { + for (String prefix : sClassPrefixList) { + try { + View view = createView(name, prefix, attrs); + if (view != null) { + return view; + } + } catch (ClassNotFoundException e) { + // In this case we want to let the base class take a crack + // at it. + } + } + + return super.onCreateView(name, attrs); + } + + public LayoutInflater cloneInContext(Context newContext) { + return new PhoneLayoutInflater(this, newContext); + } +} + diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java new file mode 100644 index 0000000..a578a6e --- /dev/null +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -0,0 +1,4945 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.policy; + +import static android.view.View.MeasureSpec.AT_MOST; +import static android.view.View.MeasureSpec.EXACTLY; +import static android.view.View.MeasureSpec.getMode; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static android.view.WindowManager.LayoutParams.*; + +import android.app.ActivityManagerNative; +import android.app.SearchManager; +import android.os.UserHandle; + +import android.view.ActionMode; +import android.view.ContextThemeWrapper; +import android.view.Display; +import android.view.Gravity; +import android.view.IRotationWatcher.Stub; +import android.view.IWindowManager; +import android.view.InputDevice; +import android.view.InputEvent; +import android.view.InputQueue; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.MotionEvent; +import android.view.SearchEvent; +import android.view.SurfaceHolder.Callback2; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewManager; +import android.view.ViewParent; +import android.view.ViewRootImpl; +import android.view.ViewStub; +import android.view.ViewTreeObserver.OnPreDrawListener; +import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; +import com.android.internal.R; +import com.android.internal.util.ScreenShapeHelper; +import com.android.internal.view.FloatingActionMode; +import com.android.internal.view.RootViewSurfaceTaker; +import com.android.internal.view.StandaloneActionMode; +import com.android.internal.view.menu.ContextMenuBuilder; +import com.android.internal.view.menu.IconMenuPresenter; +import com.android.internal.view.menu.ListMenuPresenter; +import com.android.internal.view.menu.MenuBuilder; +import com.android.internal.view.menu.MenuDialogHelper; +import com.android.internal.view.menu.MenuPresenter; +import com.android.internal.view.menu.MenuView; +import com.android.internal.widget.ActionBarContextView; +import com.android.internal.widget.BackgroundFallback; +import com.android.internal.widget.DecorContentParent; +import com.android.internal.widget.FloatingToolbar; +import com.android.internal.widget.SwipeDismissLayout; + +import android.app.ActivityManager; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.media.AudioManager; +import android.media.session.MediaController; +import android.media.session.MediaSessionLegacyHelper; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.transition.Scene; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.AndroidRuntimeException; +import android.util.DisplayMetrics; +import android.util.EventLog; +import android.util.Log; +import android.util.SparseArray; +import android.util.TypedValue; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.ProgressBar; +import android.widget.TextView; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +/** + * Android-specific Window. + * <p> + * todo: need to pull the generic functionality out into a base class + * in android.widget. + * + * @hide + */ +public class PhoneWindow extends Window implements MenuBuilder.Callback { + + private final static String TAG = "PhoneWindow"; + + private final static boolean SWEEP_OPEN_MENU = false; + + private final static int DEFAULT_BACKGROUND_FADE_DURATION_MS = 300; + + private static final int CUSTOM_TITLE_COMPATIBLE_FEATURES = DEFAULT_FEATURES | + (1 << FEATURE_CUSTOM_TITLE) | + (1 << FEATURE_CONTENT_TRANSITIONS) | + (1 << FEATURE_ACTIVITY_TRANSITIONS) | + (1 << FEATURE_ACTION_MODE_OVERLAY); + + private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet(); + + /** + * Simple callback used by the context menu and its submenus. The options + * menu submenus do not use this (their behavior is more complex). + */ + final DialogMenuCallback mContextMenuCallback = new DialogMenuCallback(FEATURE_CONTEXT_MENU); + + final TypedValue mMinWidthMajor = new TypedValue(); + final TypedValue mMinWidthMinor = new TypedValue(); + TypedValue mFixedWidthMajor; + TypedValue mFixedWidthMinor; + TypedValue mFixedHeightMajor; + TypedValue mFixedHeightMinor; + int mOutsetBottomPx; + + // This is the top-level view of the window, containing the window decor. + private DecorView mDecor; + + // This is the view in which the window contents are placed. It is either + // mDecor itself, or a child of mDecor where the contents go. + private ViewGroup mContentParent; + + private ViewGroup mContentRoot; + + Callback2 mTakeSurfaceCallback; + + InputQueue.Callback mTakeInputQueueCallback; + + private boolean mIsFloating; + + private LayoutInflater mLayoutInflater; + + private TextView mTitleView; + + private DecorContentParent mDecorContentParent; + private ActionMenuPresenterCallback mActionMenuPresenterCallback; + private PanelMenuPresenterCallback mPanelMenuPresenterCallback; + + private TransitionManager mTransitionManager; + private Scene mContentScene; + + // The icon resource has been explicitly set elsewhere + // and should not be overwritten with a default. + static final int FLAG_RESOURCE_SET_ICON = 1 << 0; + + // The logo resource has been explicitly set elsewhere + // and should not be overwritten with a default. + static final int FLAG_RESOURCE_SET_LOGO = 1 << 1; + + // The icon resource is currently configured to use the system fallback + // as no default was previously specified. Anything can override this. + static final int FLAG_RESOURCE_SET_ICON_FALLBACK = 1 << 2; + + int mResourcesSetFlags; + int mIconRes; + int mLogoRes; + + private DrawableFeatureState[] mDrawables; + + private PanelFeatureState[] mPanels; + + /** + * The panel that is prepared or opened (the most recent one if there are + * multiple panels). Shortcuts will go to this panel. It gets set in + * {@link #preparePanel} and cleared in {@link #closePanel}. + */ + private PanelFeatureState mPreparedPanel; + + /** + * The keycode that is currently held down (as a modifier) for chording. If + * this is 0, there is no key held down. + */ + private int mPanelChordingKey; + + private ImageView mLeftIconView; + + private ImageView mRightIconView; + + private ProgressBar mCircularProgressBar; + + private ProgressBar mHorizontalProgressBar; + + private int mBackgroundResource = 0; + private int mBackgroundFallbackResource = 0; + + private Drawable mBackgroundDrawable; + + private float mElevation; + + /** Whether window content should be clipped to the background outline. */ + private boolean mClipToOutline; + + private int mFrameResource = 0; + + private int mTextColor = 0; + private int mStatusBarColor = 0; + private int mNavigationBarColor = 0; + private boolean mForcedStatusBarColor = false; + private boolean mForcedNavigationBarColor = false; + + private CharSequence mTitle = null; + + private int mTitleColor = 0; + + private boolean mAlwaysReadCloseOnTouchAttr = false; + + private ContextMenuBuilder mContextMenu; + private MenuDialogHelper mContextMenuHelper; + private boolean mClosingActionMenu; + + private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE; + private MediaController mMediaController; + + private AudioManager mAudioManager; + private KeyguardManager mKeyguardManager; + + private int mUiOptions = 0; + + private boolean mInvalidatePanelMenuPosted; + private int mInvalidatePanelMenuFeatures; + private final Runnable mInvalidatePanelMenuRunnable = new Runnable() { + @Override public void run() { + for (int i = 0; i <= FEATURE_MAX; i++) { + if ((mInvalidatePanelMenuFeatures & 1 << i) != 0) { + doInvalidatePanelMenu(i); + } + } + mInvalidatePanelMenuPosted = false; + mInvalidatePanelMenuFeatures = 0; + } + }; + + private Transition mEnterTransition = null; + private Transition mReturnTransition = USE_DEFAULT_TRANSITION; + private Transition mExitTransition = null; + private Transition mReenterTransition = USE_DEFAULT_TRANSITION; + private Transition mSharedElementEnterTransition = null; + private Transition mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + private Transition mSharedElementExitTransition = null; + private Transition mSharedElementReenterTransition = USE_DEFAULT_TRANSITION; + private Boolean mAllowReturnTransitionOverlap; + private Boolean mAllowEnterTransitionOverlap; + private long mBackgroundFadeDurationMillis = -1; + private Boolean mSharedElementsUseOverlay; + + private Rect mTempRect; + + static class WindowManagerHolder { + static final IWindowManager sWindowManager = IWindowManager.Stub.asInterface( + ServiceManager.getService("window")); + } + + static final RotationWatcher sRotationWatcher = new RotationWatcher(); + + public PhoneWindow(Context context) { + super(context); + mLayoutInflater = LayoutInflater.from(context); + } + + @Override + public final void setContainer(Window container) { + super.setContainer(container); + } + + @Override + public boolean requestFeature(int featureId) { + if (mContentParent != null) { + throw new AndroidRuntimeException("requestFeature() must be called before adding content"); + } + final int features = getFeatures(); + final int newFeatures = features | (1 << featureId); + if ((newFeatures & (1 << FEATURE_CUSTOM_TITLE)) != 0 && + (newFeatures & ~CUSTOM_TITLE_COMPATIBLE_FEATURES) != 0) { + // Another feature is enabled and the user is trying to enable the custom title feature + // or custom title feature is enabled and the user is trying to enable another feature + throw new AndroidRuntimeException( + "You cannot combine custom titles with other title features"); + } + if ((features & (1 << FEATURE_NO_TITLE)) != 0 && featureId == FEATURE_ACTION_BAR) { + return false; // Ignore. No title dominates. + } + if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_NO_TITLE) { + // Remove the action bar feature if we have no title. No title dominates. + removeFeature(FEATURE_ACTION_BAR); + } + + if ((features & (1 << FEATURE_ACTION_BAR)) != 0 && featureId == FEATURE_SWIPE_TO_DISMISS) { + throw new AndroidRuntimeException( + "You cannot combine swipe dismissal and the action bar."); + } + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0 && featureId == FEATURE_ACTION_BAR) { + throw new AndroidRuntimeException( + "You cannot combine swipe dismissal and the action bar."); + } + + if (featureId == FEATURE_INDETERMINATE_PROGRESS && + getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)) { + throw new AndroidRuntimeException("You cannot use indeterminate progress on a watch."); + } + return super.requestFeature(featureId); + } + + @Override + public void setUiOptions(int uiOptions) { + mUiOptions = uiOptions; + } + + @Override + public void setUiOptions(int uiOptions, int mask) { + mUiOptions = (mUiOptions & ~mask) | (uiOptions & mask); + } + + @Override + public TransitionManager getTransitionManager() { + return mTransitionManager; + } + + @Override + public void setTransitionManager(TransitionManager tm) { + mTransitionManager = tm; + } + + @Override + public Scene getContentScene() { + return mContentScene; + } + + @Override + public void setContentView(int layoutResID) { + // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window + // decor, when theme attributes and the like are crystalized. Do not check the feature + // before this happens. + if (mContentParent == null) { + installDecor(); + } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + mContentParent.removeAllViews(); + } + + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID, + getContext()); + transitionTo(newScene); + } else { + mLayoutInflater.inflate(layoutResID, mContentParent); + } + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + @Override + public void setContentView(View view) { + setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window + // decor, when theme attributes and the like are crystalized. Do not check the feature + // before this happens. + if (mContentParent == null) { + installDecor(); + } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + mContentParent.removeAllViews(); + } + + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + view.setLayoutParams(params); + final Scene newScene = new Scene(mContentParent, view); + transitionTo(newScene); + } else { + mContentParent.addView(view, params); + } + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + if (mContentParent == null) { + installDecor(); + } + if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { + // TODO Augment the scenes/transitions API to support this. + Log.v(TAG, "addContentView does not support content transitions"); + } + mContentParent.addView(view, params); + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onContentChanged(); + } + } + + private void transitionTo(Scene scene) { + if (mContentScene == null) { + scene.enter(); + } else { + mTransitionManager.transitionTo(scene); + } + mContentScene = scene; + } + + @Override + public View getCurrentFocus() { + return mDecor != null ? mDecor.findFocus() : null; + } + + @Override + public void takeSurface(Callback2 callback) { + mTakeSurfaceCallback = callback; + } + + public void takeInputQueue(InputQueue.Callback callback) { + mTakeInputQueueCallback = callback; + } + + @Override + public boolean isFloating() { + return mIsFloating; + } + + /** + * Return a LayoutInflater instance that can be used to inflate XML view layout + * resources for use in this Window. + * + * @return LayoutInflater The shared LayoutInflater. + */ + @Override + public LayoutInflater getLayoutInflater() { + return mLayoutInflater; + } + + @Override + public void setTitle(CharSequence title) { + if (mTitleView != null) { + mTitleView.setText(title); + } else if (mDecorContentParent != null) { + mDecorContentParent.setWindowTitle(title); + } + mTitle = title; + } + + @Override + @Deprecated + public void setTitleColor(int textColor) { + if (mTitleView != null) { + mTitleView.setTextColor(textColor); + } + mTitleColor = textColor; + } + + /** + * Prepares the panel to either be opened or chorded. This creates the Menu + * instance for the panel and populates it via the Activity callbacks. + * + * @param st The panel state to prepare. + * @param event The event that triggered the preparing of the panel. + * @return Whether the panel was prepared. If the panel should not be shown, + * returns false. + */ + public final boolean preparePanel(PanelFeatureState st, KeyEvent event) { + if (isDestroyed()) { + return false; + } + + // Already prepared (isPrepared will be reset to false later) + if (st.isPrepared) { + return true; + } + + if ((mPreparedPanel != null) && (mPreparedPanel != st)) { + // Another Panel is prepared and possibly open, so close it + closePanel(mPreparedPanel, false); + } + + final Callback cb = getCallback(); + + if (cb != null) { + st.createdPanelView = cb.onCreatePanelView(st.featureId); + } + + final boolean isActionBarMenu = + (st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR); + + if (isActionBarMenu && mDecorContentParent != null) { + // Enforce ordering guarantees around events so that the action bar never + // dispatches menu-related events before the panel is prepared. + mDecorContentParent.setMenuPrepared(); + } + + if (st.createdPanelView == null) { + // Init the panel state's menu--return false if init failed + if (st.menu == null || st.refreshMenuContent) { + if (st.menu == null) { + if (!initializePanelMenu(st) || (st.menu == null)) { + return false; + } + } + + if (isActionBarMenu && mDecorContentParent != null) { + if (mActionMenuPresenterCallback == null) { + mActionMenuPresenterCallback = new ActionMenuPresenterCallback(); + } + mDecorContentParent.setMenu(st.menu, mActionMenuPresenterCallback); + } + + // Call callback, and return if it doesn't want to display menu. + + // Creating the panel menu will involve a lot of manipulation; + // don't dispatch change events to presenters until we're done. + st.menu.stopDispatchingItemsChanged(); + if ((cb == null) || !cb.onCreatePanelMenu(st.featureId, st.menu)) { + // Ditch the menu created above + st.setMenu(null); + + if (isActionBarMenu && mDecorContentParent != null) { + // Don't show it in the action bar either + mDecorContentParent.setMenu(null, mActionMenuPresenterCallback); + } + + return false; + } + + st.refreshMenuContent = false; + } + + // Callback and return if the callback does not want to show the menu + + // Preparing the panel menu can involve a lot of manipulation; + // don't dispatch change events to presenters until we're done. + st.menu.stopDispatchingItemsChanged(); + + // Restore action view state before we prepare. This gives apps + // an opportunity to override frozen/restored state in onPrepare. + if (st.frozenActionViewState != null) { + st.menu.restoreActionViewStates(st.frozenActionViewState); + st.frozenActionViewState = null; + } + + if (!cb.onPreparePanel(st.featureId, st.createdPanelView, st.menu)) { + if (isActionBarMenu && mDecorContentParent != null) { + // The app didn't want to show the menu for now but it still exists. + // Clear it out of the action bar. + mDecorContentParent.setMenu(null, mActionMenuPresenterCallback); + } + st.menu.startDispatchingItemsChanged(); + return false; + } + + // Set the proper keymap + KeyCharacterMap kmap = KeyCharacterMap.load( + event != null ? event.getDeviceId() : KeyCharacterMap.VIRTUAL_KEYBOARD); + st.qwertyMode = kmap.getKeyboardType() != KeyCharacterMap.NUMERIC; + st.menu.setQwertyMode(st.qwertyMode); + st.menu.startDispatchingItemsChanged(); + } + + // Set other state + st.isPrepared = true; + st.isHandled = false; + mPreparedPanel = st; + + return true; + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + // Action bars handle their own menu state + if (mDecorContentParent == null) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if ((st != null) && (st.menu != null)) { + if (st.isOpen) { + // Freeze state + final Bundle state = new Bundle(); + if (st.iconMenuPresenter != null) { + st.iconMenuPresenter.saveHierarchyState(state); + } + if (st.listMenuPresenter != null) { + st.listMenuPresenter.saveHierarchyState(state); + } + + // Remove the menu views since they need to be recreated + // according to the new configuration + clearMenuViews(st); + + // Re-open the same menu + reopenMenu(false); + + // Restore state + if (st.iconMenuPresenter != null) { + st.iconMenuPresenter.restoreHierarchyState(state); + } + if (st.listMenuPresenter != null) { + st.listMenuPresenter.restoreHierarchyState(state); + } + + } else { + // Clear menu views so on next menu opening, it will use + // the proper layout + clearMenuViews(st); + } + } + } + } + + private static void clearMenuViews(PanelFeatureState st) { + // This can be called on config changes, so we should make sure + // the views will be reconstructed based on the new orientation, etc. + + // Allow the callback to create a new panel view + st.createdPanelView = null; + + // Causes the decor view to be recreated + st.refreshDecorView = true; + + st.clearMenuPresenters(); + } + + @Override + public final void openPanel(int featureId, KeyEvent event) { + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + mDecorContentParent.showOverflowMenu(); + } else { + openPanel(getPanelState(featureId, true), event); + } + } + + private void openPanel(final PanelFeatureState st, KeyEvent event) { + // System.out.println("Open panel: isOpen=" + st.isOpen); + + // Already open, return + if (st.isOpen || isDestroyed()) { + return; + } + + // Don't open an options panel for honeycomb apps on xlarge devices. + // (The app should be using an action bar for menu items.) + if (st.featureId == FEATURE_OPTIONS_PANEL) { + Context context = getContext(); + Configuration config = context.getResources().getConfiguration(); + boolean isXLarge = (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) == + Configuration.SCREENLAYOUT_SIZE_XLARGE; + boolean isHoneycombApp = context.getApplicationInfo().targetSdkVersion >= + android.os.Build.VERSION_CODES.HONEYCOMB; + + if (isXLarge && isHoneycombApp) { + return; + } + } + + Callback cb = getCallback(); + if ((cb != null) && (!cb.onMenuOpened(st.featureId, st.menu))) { + // Callback doesn't want the menu to open, reset any state + closePanel(st, true); + return; + } + + final WindowManager wm = getWindowManager(); + if (wm == null) { + return; + } + + // Prepare panel (should have been done before, but just in case) + if (!preparePanel(st, event)) { + return; + } + + int width = WRAP_CONTENT; + if (st.decorView == null || st.refreshDecorView) { + if (st.decorView == null) { + // Initialize the panel decor, this will populate st.decorView + if (!initializePanelDecor(st) || (st.decorView == null)) + return; + } else if (st.refreshDecorView && (st.decorView.getChildCount() > 0)) { + // Decor needs refreshing, so remove its views + st.decorView.removeAllViews(); + } + + // This will populate st.shownPanelView + if (!initializePanelContent(st) || !st.hasPanelItems()) { + return; + } + + ViewGroup.LayoutParams lp = st.shownPanelView.getLayoutParams(); + if (lp == null) { + lp = new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + } + + int backgroundResId; + if (lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + // If the contents is fill parent for the width, set the + // corresponding background + backgroundResId = st.fullBackground; + width = MATCH_PARENT; + } else { + // Otherwise, set the normal panel background + backgroundResId = st.background; + } + st.decorView.setWindowBackground(getContext().getDrawable( + backgroundResId)); + + ViewParent shownPanelParent = st.shownPanelView.getParent(); + if (shownPanelParent != null && shownPanelParent instanceof ViewGroup) { + ((ViewGroup) shownPanelParent).removeView(st.shownPanelView); + } + st.decorView.addView(st.shownPanelView, lp); + + /* + * Give focus to the view, if it or one of its children does not + * already have it. + */ + if (!st.shownPanelView.hasFocus()) { + st.shownPanelView.requestFocus(); + } + } else if (!st.isInListMode()) { + width = MATCH_PARENT; + } else if (st.createdPanelView != null) { + // If we already had a panel view, carry width=MATCH_PARENT through + // as we did above when it was created. + ViewGroup.LayoutParams lp = st.createdPanelView.getLayoutParams(); + if (lp != null && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + width = MATCH_PARENT; + } + } + + st.isHandled = false; + + WindowManager.LayoutParams lp = new WindowManager.LayoutParams( + width, WRAP_CONTENT, + st.x, st.y, WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG, + WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH, + st.decorView.mDefaultOpacity); + + if (st.isCompact) { + lp.gravity = getOptionsPanelGravity(); + sRotationWatcher.addWindow(this); + } else { + lp.gravity = st.gravity; + } + + lp.windowAnimations = st.windowAnimations; + + wm.addView(st.decorView, lp); + st.isOpen = true; + // Log.v(TAG, "Adding main menu to window manager."); + } + + @Override + public final void closePanel(int featureId) { + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + mDecorContentParent.hideOverflowMenu(); + } else if (featureId == FEATURE_CONTEXT_MENU) { + closeContextMenu(); + } else { + closePanel(getPanelState(featureId, true), true); + } + } + + /** + * Closes the given panel. + * + * @param st The panel to be closed. + * @param doCallback Whether to notify the callback that the panel was + * closed. If the panel is in the process of re-opening or + * opening another panel (e.g., menu opening a sub menu), the + * callback should not happen and this variable should be false. + * In addition, this method internally will only perform the + * callback if the panel is open. + */ + public final void closePanel(PanelFeatureState st, boolean doCallback) { + // System.out.println("Close panel: isOpen=" + st.isOpen); + if (doCallback && st.featureId == FEATURE_OPTIONS_PANEL && + mDecorContentParent != null && mDecorContentParent.isOverflowMenuShowing()) { + checkCloseActionMenu(st.menu); + return; + } + + final ViewManager wm = getWindowManager(); + if ((wm != null) && st.isOpen) { + if (st.decorView != null) { + wm.removeView(st.decorView); + // Log.v(TAG, "Removing main menu from window manager."); + if (st.isCompact) { + sRotationWatcher.removeWindow(this); + } + } + + if (doCallback) { + callOnPanelClosed(st.featureId, st, null); + } + } + + st.isPrepared = false; + st.isHandled = false; + st.isOpen = false; + + // This view is no longer shown, so null it out + st.shownPanelView = null; + + if (st.isInExpandedMode) { + // Next time the menu opens, it should not be in expanded mode, so + // force a refresh of the decor + st.refreshDecorView = true; + st.isInExpandedMode = false; + } + + if (mPreparedPanel == st) { + mPreparedPanel = null; + mPanelChordingKey = 0; + } + } + + void checkCloseActionMenu(Menu menu) { + if (mClosingActionMenu) { + return; + } + + mClosingActionMenu = true; + mDecorContentParent.dismissPopups(); + Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onPanelClosed(FEATURE_ACTION_BAR, menu); + } + mClosingActionMenu = false; + } + + @Override + public final void togglePanel(int featureId, KeyEvent event) { + PanelFeatureState st = getPanelState(featureId, true); + if (st.isOpen) { + closePanel(st, true); + } else { + openPanel(st, event); + } + } + + @Override + public void invalidatePanelMenu(int featureId) { + mInvalidatePanelMenuFeatures |= 1 << featureId; + + if (!mInvalidatePanelMenuPosted && mDecor != null) { + mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuPosted = true; + } + } + + void doPendingInvalidatePanelMenu() { + if (mInvalidatePanelMenuPosted) { + mDecor.removeCallbacks(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuRunnable.run(); + } + } + + void doInvalidatePanelMenu(int featureId) { + PanelFeatureState st = getPanelState(featureId, false); + if (st == null) { + return; + } + Bundle savedActionViewStates = null; + if (st.menu != null) { + savedActionViewStates = new Bundle(); + st.menu.saveActionViewStates(savedActionViewStates); + if (savedActionViewStates.size() > 0) { + st.frozenActionViewState = savedActionViewStates; + } + // This will be started again when the panel is prepared. + st.menu.stopDispatchingItemsChanged(); + st.menu.clear(); + } + st.refreshMenuContent = true; + st.refreshDecorView = true; + + // Prepare the options panel if we have an action bar + if ((featureId == FEATURE_ACTION_BAR || featureId == FEATURE_OPTIONS_PANEL) + && mDecorContentParent != null) { + st = getPanelState(Window.FEATURE_OPTIONS_PANEL, false); + if (st != null) { + st.isPrepared = false; + preparePanel(st, null); + } + } + } + + /** + * Called when the panel key is pushed down. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + * @return Whether the key was handled. + */ + public final boolean onKeyDownPanel(int featureId, KeyEvent event) { + final int keyCode = event.getKeyCode(); + + if (event.getRepeatCount() == 0) { + // The panel key was pushed, so set the chording key + mPanelChordingKey = keyCode; + + PanelFeatureState st = getPanelState(featureId, false); + if (st != null && !st.isOpen) { + return preparePanel(st, event); + } + } + + return false; + } + + /** + * Called when the panel key is released. + * @param featureId The feature ID of the relevant panel (defaults to FEATURE_OPTIONS_PANEL}. + * @param event The key event. + */ + public final void onKeyUpPanel(int featureId, KeyEvent event) { + // The panel key was released, so clear the chording key + if (mPanelChordingKey != 0) { + mPanelChordingKey = 0; + + final PanelFeatureState st = getPanelState(featureId, false); + + if (event.isCanceled() || (mDecor != null && mDecor.mPrimaryActionMode != null) || + (st == null)) { + return; + } + + boolean playSoundEffect = false; + if (featureId == FEATURE_OPTIONS_PANEL && mDecorContentParent != null && + mDecorContentParent.canShowOverflowMenu() && + !ViewConfiguration.get(getContext()).hasPermanentMenuKey()) { + if (!mDecorContentParent.isOverflowMenuShowing()) { + if (!isDestroyed() && preparePanel(st, event)) { + playSoundEffect = mDecorContentParent.showOverflowMenu(); + } + } else { + playSoundEffect = mDecorContentParent.hideOverflowMenu(); + } + } else { + if (st.isOpen || st.isHandled) { + + // Play the sound effect if the user closed an open menu (and not if + // they just released a menu shortcut) + playSoundEffect = st.isOpen; + + // Close menu + closePanel(st, true); + + } else if (st.isPrepared) { + boolean show = true; + if (st.refreshMenuContent) { + // Something may have invalidated the menu since we prepared it. + // Re-prepare it to refresh. + st.isPrepared = false; + show = preparePanel(st, event); + } + + if (show) { + // Write 'menu opened' to event log + EventLog.writeEvent(50001, 0); + + // Show menu + openPanel(st, event); + + playSoundEffect = true; + } + } + } + + if (playSoundEffect) { + AudioManager audioManager = (AudioManager) getContext().getSystemService( + Context.AUDIO_SERVICE); + if (audioManager != null) { + audioManager.playSoundEffect(AudioManager.FX_KEY_CLICK); + } else { + Log.w(TAG, "Couldn't get audio manager"); + } + } + } + } + + @Override + public final void closeAllPanels() { + final ViewManager wm = getWindowManager(); + if (wm == null) { + return; + } + + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null) { + closePanel(panel, true); + } + } + + closeContextMenu(); + } + + /** + * Closes the context menu. This notifies the menu logic of the close, along + * with dismissing it from the UI. + */ + private synchronized void closeContextMenu() { + if (mContextMenu != null) { + mContextMenu.close(); + dismissContextMenu(); + } + } + + /** + * Dismisses just the context menu UI. To close the context menu, use + * {@link #closeContextMenu()}. + */ + private synchronized void dismissContextMenu() { + mContextMenu = null; + + if (mContextMenuHelper != null) { + mContextMenuHelper.dismiss(); + mContextMenuHelper = null; + } + } + + @Override + public boolean performPanelShortcut(int featureId, int keyCode, KeyEvent event, int flags) { + return performPanelShortcut(getPanelState(featureId, false), keyCode, event, flags); + } + + private boolean performPanelShortcut(PanelFeatureState st, int keyCode, KeyEvent event, + int flags) { + if (event.isSystem() || (st == null)) { + return false; + } + + boolean handled = false; + + // Only try to perform menu shortcuts if preparePanel returned true (possible false + // return value from application not wanting to show the menu). + if ((st.isPrepared || preparePanel(st, event)) && st.menu != null) { + // The menu is prepared now, perform the shortcut on it + handled = st.menu.performShortcut(keyCode, event, flags); + } + + if (handled) { + // Mark as handled + st.isHandled = true; + + // Only close down the menu if we don't have an action bar keeping it open. + if ((flags & Menu.FLAG_PERFORM_NO_CLOSE) == 0 && mDecorContentParent == null) { + closePanel(st, true); + } + } + + return handled; + } + + @Override + public boolean performPanelIdentifierAction(int featureId, int id, int flags) { + + PanelFeatureState st = getPanelState(featureId, true); + if (!preparePanel(st, new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU))) { + return false; + } + if (st.menu == null) { + return false; + } + + boolean res = st.menu.performIdentifierAction(id, flags); + + // Only close down the menu if we don't have an action bar keeping it open. + if (mDecorContentParent == null) { + closePanel(st, true); + } + + return res; + } + + public PanelFeatureState findMenuPanel(Menu menu) { + final PanelFeatureState[] panels = mPanels; + final int N = panels != null ? panels.length : 0; + for (int i = 0; i < N; i++) { + final PanelFeatureState panel = panels[i]; + if (panel != null && panel.menu == menu) { + return panel; + } + } + return null; + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + final PanelFeatureState panel = findMenuPanel(menu.getRootMenu()); + if (panel != null) { + return cb.onMenuItemSelected(panel.featureId, item); + } + } + return false; + } + + public void onMenuModeChange(MenuBuilder menu) { + reopenMenu(true); + } + + private void reopenMenu(boolean toggleMenuMode) { + if (mDecorContentParent != null && mDecorContentParent.canShowOverflowMenu() && + (!ViewConfiguration.get(getContext()).hasPermanentMenuKey() || + mDecorContentParent.isOverflowMenuShowPending())) { + final Callback cb = getCallback(); + if (!mDecorContentParent.isOverflowMenuShowing() || !toggleMenuMode) { + if (cb != null && !isDestroyed()) { + // If we have a menu invalidation pending, do it now. + if (mInvalidatePanelMenuPosted && + (mInvalidatePanelMenuFeatures & (1 << FEATURE_OPTIONS_PANEL)) != 0) { + mDecor.removeCallbacks(mInvalidatePanelMenuRunnable); + mInvalidatePanelMenuRunnable.run(); + } + + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + + // If we don't have a menu or we're waiting for a full content refresh, + // forget it. This is a lingering event that no longer matters. + if (st != null && st.menu != null && !st.refreshMenuContent && + cb.onPreparePanel(FEATURE_OPTIONS_PANEL, st.createdPanelView, st.menu)) { + cb.onMenuOpened(FEATURE_ACTION_BAR, st.menu); + mDecorContentParent.showOverflowMenu(); + } + } + } else { + mDecorContentParent.hideOverflowMenu(); + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && cb != null && !isDestroyed()) { + cb.onPanelClosed(FEATURE_ACTION_BAR, st.menu); + } + } + return; + } + + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + + if (st == null) { + return; + } + + // Save the future expanded mode state since closePanel will reset it + boolean newExpandedMode = toggleMenuMode ? !st.isInExpandedMode : st.isInExpandedMode; + + st.refreshDecorView = true; + closePanel(st, false); + + // Set the expanded mode state + st.isInExpandedMode = newExpandedMode; + + openPanel(st, null); + } + + /** + * Initializes the menu associated with the given panel feature state. You + * must at the very least set PanelFeatureState.menu to the Menu to be + * associated with the given panel state. The default implementation creates + * a new menu for the panel state. + * + * @param st The panel whose menu is being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelMenu(final PanelFeatureState st) { + Context context = getContext(); + + // If we have an action bar, initialize the menu with the right theme. + if ((st.featureId == FEATURE_OPTIONS_PANEL || st.featureId == FEATURE_ACTION_BAR) && + mDecorContentParent != null) { + final TypedValue outValue = new TypedValue(); + final Theme baseTheme = context.getTheme(); + baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true); + + Theme widgetTheme = null; + if (outValue.resourceId != 0) { + widgetTheme = context.getResources().newTheme(); + widgetTheme.setTo(baseTheme); + widgetTheme.applyStyle(outValue.resourceId, true); + widgetTheme.resolveAttribute( + R.attr.actionBarWidgetTheme, outValue, true); + } else { + baseTheme.resolveAttribute( + R.attr.actionBarWidgetTheme, outValue, true); + } + + if (outValue.resourceId != 0) { + if (widgetTheme == null) { + widgetTheme = context.getResources().newTheme(); + widgetTheme.setTo(baseTheme); + } + widgetTheme.applyStyle(outValue.resourceId, true); + } + + if (widgetTheme != null) { + context = new ContextThemeWrapper(context, 0); + context.getTheme().setTo(widgetTheme); + } + } + + final MenuBuilder menu = new MenuBuilder(context); + menu.setCallback(this); + st.setMenu(menu); + + return true; + } + + /** + * Perform initial setup of a panel. This should at the very least set the + * style information in the PanelFeatureState and must set + * PanelFeatureState.decor to the panel's window decor view. + * + * @param st The panel being initialized. + */ + protected boolean initializePanelDecor(PanelFeatureState st) { + st.decorView = new DecorView(getContext(), st.featureId); + st.gravity = Gravity.CENTER | Gravity.BOTTOM; + st.setStyle(getContext()); + TypedArray a = getContext().obtainStyledAttributes(null, + R.styleable.Window, 0, st.listPresenterTheme); + final float elevation = a.getDimension(R.styleable.Window_windowElevation, 0); + if (elevation != 0) { + st.decorView.setElevation(elevation); + } + a.recycle(); + + return true; + } + + /** + * Determine the gravity value for the options panel. This can + * differ in compact mode. + * + * @return gravity value to use for the panel window + */ + private int getOptionsPanelGravity() { + try { + return WindowManagerHolder.sWindowManager.getPreferredOptionsPanelGravity(); + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't getOptionsPanelGravity; using default", ex); + return Gravity.CENTER | Gravity.BOTTOM; + } + } + + void onOptionsPanelRotationChanged() { + final PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st == null) return; + + final WindowManager.LayoutParams lp = st.decorView != null ? + (WindowManager.LayoutParams) st.decorView.getLayoutParams() : null; + if (lp != null) { + lp.gravity = getOptionsPanelGravity(); + final ViewManager wm = getWindowManager(); + if (wm != null) { + wm.updateViewLayout(st.decorView, lp); + } + } + } + + /** + * Initializes the panel associated with the panel feature state. You must + * at the very least set PanelFeatureState.panel to the View implementing + * its contents. The default implementation gets the panel from the menu. + * + * @param st The panel state being initialized. + * @return Whether the initialization was successful. + */ + protected boolean initializePanelContent(PanelFeatureState st) { + if (st.createdPanelView != null) { + st.shownPanelView = st.createdPanelView; + return true; + } + + if (st.menu == null) { + return false; + } + + if (mPanelMenuPresenterCallback == null) { + mPanelMenuPresenterCallback = new PanelMenuPresenterCallback(); + } + + MenuView menuView = st.isInListMode() + ? st.getListMenuView(getContext(), mPanelMenuPresenterCallback) + : st.getIconMenuView(getContext(), mPanelMenuPresenterCallback); + + st.shownPanelView = (View) menuView; + + if (st.shownPanelView != null) { + // Use the menu View's default animations if it has any + final int defaultAnimations = menuView.getWindowAnimations(); + if (defaultAnimations != 0) { + st.windowAnimations = defaultAnimations; + } + return true; + } else { + return false; + } + } + + @Override + public boolean performContextMenuIdentifierAction(int id, int flags) { + return (mContextMenu != null) ? mContextMenu.performIdentifierAction(id, flags) : false; + } + + @Override + public final void setElevation(float elevation) { + mElevation = elevation; + if (mDecor != null) { + mDecor.setElevation(elevation); + } + dispatchWindowAttributesChanged(getAttributes()); + } + + @Override + public final void setClipToOutline(boolean clipToOutline) { + mClipToOutline = clipToOutline; + if (mDecor != null) { + mDecor.setClipToOutline(clipToOutline); + } + } + + @Override + public final void setBackgroundDrawable(Drawable drawable) { + if (drawable != mBackgroundDrawable || mBackgroundResource != 0) { + mBackgroundResource = 0; + mBackgroundDrawable = drawable; + if (mDecor != null) { + mDecor.setWindowBackground(drawable); + } + if (mBackgroundFallbackResource != 0) { + mDecor.setBackgroundFallback(drawable != null ? 0 : mBackgroundFallbackResource); + } + } + } + + @Override + public final void setFeatureDrawableResource(int featureId, int resId) { + if (resId != 0) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.resid != resId) { + st.resid = resId; + st.uri = null; + st.local = getContext().getDrawable(resId); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawableUri(int featureId, Uri uri) { + if (uri != null) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.uri == null || !st.uri.equals(uri)) { + st.resid = 0; + st.uri = uri; + st.local = loadImageURI(uri); + updateDrawable(featureId, st, false); + } + } else { + setFeatureDrawable(featureId, null); + } + } + + @Override + public final void setFeatureDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.resid = 0; + st.uri = null; + if (st.local != drawable) { + st.local = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public void setFeatureDrawableAlpha(int featureId, int alpha) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.alpha != alpha) { + st.alpha = alpha; + updateDrawable(featureId, st, false); + } + } + + protected final void setFeatureDefaultDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + if (st.def != drawable) { + st.def = drawable; + updateDrawable(featureId, st, false); + } + } + + @Override + public final void setFeatureInt(int featureId, int value) { + // XXX Should do more management (as with drawable features) to + // deal with interactions between multiple window policies. + updateInt(featureId, value, false); + } + + /** + * Update the state of a drawable feature. This should be called, for every + * drawable feature supported, as part of onActive(), to make sure that the + * contents of a containing window is properly updated. + * + * @see #onActive + * @param featureId The desired drawable feature to change. + * @param fromActive Always true when called from onActive(). + */ + protected final void updateDrawable(int featureId, boolean fromActive) { + final DrawableFeatureState st = getDrawableState(featureId, false); + if (st != null) { + updateDrawable(featureId, st, fromActive); + } + } + + /** + * Called when a Drawable feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param drawable The new Drawable to show, or null if none. + * @param alpha The new alpha blending of the Drawable. + */ + protected void onDrawableChanged(int featureId, Drawable drawable, int alpha) { + ImageView view; + if (featureId == FEATURE_LEFT_ICON) { + view = getLeftIconView(); + } else if (featureId == FEATURE_RIGHT_ICON) { + view = getRightIconView(); + } else { + return; + } + + if (drawable != null) { + drawable.setAlpha(alpha); + view.setImageDrawable(drawable); + view.setVisibility(View.VISIBLE); + } else { + view.setVisibility(View.GONE); + } + } + + /** + * Called when an int feature changes, for the window to update its + * graphics. + * + * @param featureId The feature being changed. + * @param value The new integer value. + */ + protected void onIntChanged(int featureId, int value) { + if (featureId == FEATURE_PROGRESS || featureId == FEATURE_INDETERMINATE_PROGRESS) { + updateProgressBars(value); + } else if (featureId == FEATURE_CUSTOM_TITLE) { + FrameLayout titleContainer = (FrameLayout) findViewById(R.id.title_container); + if (titleContainer != null) { + mLayoutInflater.inflate(value, titleContainer); + } + } + } + + /** + * Updates the progress bars that are shown in the title bar. + * + * @param value Can be one of {@link Window#PROGRESS_VISIBILITY_ON}, + * {@link Window#PROGRESS_VISIBILITY_OFF}, + * {@link Window#PROGRESS_INDETERMINATE_ON}, + * {@link Window#PROGRESS_INDETERMINATE_OFF}, or a value + * starting at {@link Window#PROGRESS_START} through + * {@link Window#PROGRESS_END} for setting the default + * progress (if {@link Window#PROGRESS_END} is given, + * the progress bar widgets in the title will be hidden after an + * animation), a value between + * {@link Window#PROGRESS_SECONDARY_START} - + * {@link Window#PROGRESS_SECONDARY_END} for the + * secondary progress (if + * {@link Window#PROGRESS_SECONDARY_END} is given, the + * progress bar widgets will still be shown with the secondary + * progress bar will be completely filled in.) + */ + private void updateProgressBars(int value) { + ProgressBar circularProgressBar = getCircularProgressBar(true); + ProgressBar horizontalProgressBar = getHorizontalProgressBar(true); + + final int features = getLocalFeatures(); + if (value == PROGRESS_VISIBILITY_ON) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + if (horizontalProgressBar != null) { + int level = horizontalProgressBar.getProgress(); + int visibility = (horizontalProgressBar.isIndeterminate() || level < 10000) ? + View.VISIBLE : View.INVISIBLE; + horizontalProgressBar.setVisibility(visibility); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + if (circularProgressBar != null) { + circularProgressBar.setVisibility(View.VISIBLE); + } else { + Log.e(TAG, "Circular progress bar not located in current window decor"); + } + } + } else if (value == PROGRESS_VISIBILITY_OFF) { + if ((features & (1 << FEATURE_PROGRESS)) != 0) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setVisibility(View.GONE); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + if (circularProgressBar != null) { + circularProgressBar.setVisibility(View.GONE); + } else { + Log.e(TAG, "Circular progress bar not located in current window decor"); + } + } + } else if (value == PROGRESS_INDETERMINATE_ON) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setIndeterminate(true); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } else if (value == PROGRESS_INDETERMINATE_OFF) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setIndeterminate(false); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + } else if (PROGRESS_START <= value && value <= PROGRESS_END) { + // We want to set the progress value before testing for visibility + // so that when the progress bar becomes visible again, it has the + // correct level. + if (horizontalProgressBar != null) { + horizontalProgressBar.setProgress(value - PROGRESS_START); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + + if (value < PROGRESS_END) { + showProgressBars(horizontalProgressBar, circularProgressBar); + } else { + hideProgressBars(horizontalProgressBar, circularProgressBar); + } + } else if (PROGRESS_SECONDARY_START <= value && value <= PROGRESS_SECONDARY_END) { + if (horizontalProgressBar != null) { + horizontalProgressBar.setSecondaryProgress(value - PROGRESS_SECONDARY_START); + } else { + Log.e(TAG, "Horizontal progress bar not located in current window decor"); + } + + showProgressBars(horizontalProgressBar, circularProgressBar); + } + + } + + private void showProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar != null && spinnyProgressBar.getVisibility() == View.INVISIBLE) { + spinnyProgressBar.setVisibility(View.VISIBLE); + } + // Only show the progress bars if the primary progress is not complete + if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null && + horizontalProgressBar.getProgress() < 10000) { + horizontalProgressBar.setVisibility(View.VISIBLE); + } + } + + private void hideProgressBars(ProgressBar horizontalProgressBar, ProgressBar spinnyProgressBar) { + final int features = getLocalFeatures(); + Animation anim = AnimationUtils.loadAnimation(getContext(), R.anim.fade_out); + anim.setDuration(1000); + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0 && + spinnyProgressBar != null && + spinnyProgressBar.getVisibility() == View.VISIBLE) { + spinnyProgressBar.startAnimation(anim); + spinnyProgressBar.setVisibility(View.INVISIBLE); + } + if ((features & (1 << FEATURE_PROGRESS)) != 0 && horizontalProgressBar != null && + horizontalProgressBar.getVisibility() == View.VISIBLE) { + horizontalProgressBar.startAnimation(anim); + horizontalProgressBar.setVisibility(View.INVISIBLE); + } + } + + @Override + public void setIcon(int resId) { + mIconRes = resId; + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON; + mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK; + if (mDecorContentParent != null) { + mDecorContentParent.setIcon(resId); + } + } + + @Override + public void setDefaultIcon(int resId) { + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0) { + return; + } + mIconRes = resId; + if (mDecorContentParent != null && (!mDecorContentParent.hasIcon() || + (mResourcesSetFlags & FLAG_RESOURCE_SET_ICON_FALLBACK) != 0)) { + if (resId != 0) { + mDecorContentParent.setIcon(resId); + mResourcesSetFlags &= ~FLAG_RESOURCE_SET_ICON_FALLBACK; + } else { + mDecorContentParent.setIcon( + getContext().getPackageManager().getDefaultActivityIcon()); + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; + } + } + } + + @Override + public void setLogo(int resId) { + mLogoRes = resId; + mResourcesSetFlags |= FLAG_RESOURCE_SET_LOGO; + if (mDecorContentParent != null) { + mDecorContentParent.setLogo(resId); + } + } + + @Override + public void setDefaultLogo(int resId) { + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0) { + return; + } + mLogoRes = resId; + if (mDecorContentParent != null && !mDecorContentParent.hasLogo()) { + mDecorContentParent.setLogo(resId); + } + } + + @Override + public void setLocalFocus(boolean hasFocus, boolean inTouchMode) { + getViewRootImpl().windowFocusChanged(hasFocus, inTouchMode); + + } + + @Override + public void injectInputEvent(InputEvent event) { + getViewRootImpl().dispatchInputEvent(event); + } + + private ViewRootImpl getViewRootImpl() { + if (mDecor != null) { + ViewRootImpl viewRootImpl = mDecor.getViewRootImpl(); + if (viewRootImpl != null) { + return viewRootImpl; + } + } + throw new IllegalStateException("view not added"); + } + + /** + * Request that key events come to this activity. Use this if your activity + * has no views with focus, but the activity still wants a chance to process + * key events. + */ + @Override + public void takeKeyEvents(boolean get) { + mDecor.setFocusable(get); + } + + @Override + public boolean superDispatchKeyEvent(KeyEvent event) { + return mDecor.superDispatchKeyEvent(event); + } + + @Override + public boolean superDispatchKeyShortcutEvent(KeyEvent event) { + return mDecor.superDispatchKeyShortcutEvent(event); + } + + @Override + public boolean superDispatchTouchEvent(MotionEvent event) { + return mDecor.superDispatchTouchEvent(event); + } + + @Override + public boolean superDispatchTrackballEvent(MotionEvent event) { + return mDecor.superDispatchTrackballEvent(event); + } + + @Override + public boolean superDispatchGenericMotionEvent(MotionEvent event) { + return mDecor.superDispatchGenericMotionEvent(event); + } + + /** + * A key was pressed down and not handled by anything else in the window. + * + * @see #onKeyUp + * @see android.view.KeyEvent + */ + protected boolean onKeyDown(int featureId, int keyCode, KeyEvent event) { + /* **************************************************************************** + * HOW TO DECIDE WHERE YOUR KEY HANDLING GOES. + * + * If your key handling must happen before the app gets a crack at the event, + * it goes in PhoneWindowManager. + * + * If your key handling should happen in all windows, and does not depend on + * the state of the current application, other than that the current + * application can override the behavior by handling the event itself, it + * should go in PhoneFallbackEventHandler. + * + * Only if your handling depends on the window, and the fact that it has + * a DecorView, should it go here. + * ****************************************************************************/ + + final KeyEvent.DispatcherState dispatcher = + mDecor != null ? mDecor.getKeyDispatcherState() : null; + //Log.i(TAG, "Key down: repeat=" + event.getRepeatCount() + // + " flags=0x" + Integer.toHexString(event.getFlags())); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_VOLUME_MUTE: { + int direction = 0; + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + direction = AudioManager.ADJUST_RAISE; + break; + case KeyEvent.KEYCODE_VOLUME_DOWN: + direction = AudioManager.ADJUST_LOWER; + break; + case KeyEvent.KEYCODE_VOLUME_MUTE: + direction = AudioManager.ADJUST_TOGGLE_MUTE; + break; + } + // If we have a session send it the volume command, otherwise + // use the suggested stream. + if (mMediaController != null) { + mMediaController.adjustVolume(direction, AudioManager.FLAG_SHOW_UI); + } else { + MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy( + mVolumeControlStreamType, direction, + AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_VIBRATE + | AudioManager.FLAG_FROM_KEY); + } + return true; + } + // These are all the recognized media key codes in + // KeyEvent.isMediaKey() + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + if (mMediaController != null) { + if (mMediaController.dispatchMediaButtonEvent(event)) { + return true; + } + } + return false; + } + + case KeyEvent.KEYCODE_MENU: { + onKeyDownPanel((featureId < 0) ? FEATURE_OPTIONS_PANEL : featureId, event); + return true; + } + + case KeyEvent.KEYCODE_BACK: { + if (event.getRepeatCount() > 0) break; + if (featureId < 0) break; + // Currently don't do anything with long press. + if (dispatcher != null) { + dispatcher.startTracking(event, this); + } + return true; + } + + } + + return false; + } + + private KeyguardManager getKeyguardManager() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager) getContext().getSystemService( + Context.KEYGUARD_SERVICE); + } + return mKeyguardManager; + } + + AudioManager getAudioManager() { + if (mAudioManager == null) { + mAudioManager = (AudioManager)getContext().getSystemService(Context.AUDIO_SERVICE); + } + return mAudioManager; + } + + /** + * A key was released and not handled by anything else in the window. + * + * @see #onKeyDown + * @see android.view.KeyEvent + */ + protected boolean onKeyUp(int featureId, int keyCode, KeyEvent event) { + final KeyEvent.DispatcherState dispatcher = + mDecor != null ? mDecor.getKeyDispatcherState() : null; + if (dispatcher != null) { + dispatcher.handleUpEvent(event); + } + //Log.i(TAG, "Key up: repeat=" + event.getRepeatCount() + // + " flags=0x" + Integer.toHexString(event.getFlags())); + + switch (keyCode) { + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: { + final int flags = AudioManager.FLAG_PLAY_SOUND | AudioManager.FLAG_VIBRATE + | AudioManager.FLAG_FROM_KEY; + // If we have a session send it the volume command, otherwise + // use the suggested stream. + if (mMediaController != null) { + mMediaController.adjustVolume(0, flags); + } else { + MediaSessionLegacyHelper.getHelper(getContext()).sendAdjustVolumeBy( + mVolumeControlStreamType, 0, flags); + } + return true; + } + case KeyEvent.KEYCODE_VOLUME_MUTE: { + // Similar code is in PhoneFallbackEventHandler in case the window + // doesn't have one of these. In this case, we execute it here and + // eat the event instead, because we have mVolumeControlStreamType + // and they don't. + getAudioManager().handleKeyUp(event, mVolumeControlStreamType); + return true; + } + // These are all the recognized media key codes in + // KeyEvent.isMediaKey() + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + if (mMediaController != null) { + if (mMediaController.dispatchMediaButtonEvent(event)) { + return true; + } + } + return false; + } + + case KeyEvent.KEYCODE_MENU: { + onKeyUpPanel(featureId < 0 ? FEATURE_OPTIONS_PANEL : featureId, + event); + return true; + } + + case KeyEvent.KEYCODE_BACK: { + if (featureId < 0) break; + if (event.isTracking() && !event.isCanceled()) { + if (featureId == FEATURE_OPTIONS_PANEL) { + PanelFeatureState st = getPanelState(featureId, false); + if (st != null && st.isInExpandedMode) { + // If the user is in an expanded menu and hits back, it + // should go back to the icon menu + reopenMenu(true); + return true; + } + } + closePanel(featureId); + return true; + } + break; + } + + case KeyEvent.KEYCODE_SEARCH: { + /* + * Do this in onKeyUp since the Search key is also used for + * chording quick launch shortcuts. + */ + if (getKeyguardManager().inKeyguardRestrictedInputMode()) { + break; + } + if (event.isTracking() && !event.isCanceled()) { + launchDefaultSearch(event); + } + return true; + } + } + + return false; + } + + @Override + protected void onActive() { + } + + @Override + public final View getDecorView() { + if (mDecor == null) { + installDecor(); + } + return mDecor; + } + + @Override + public final View peekDecorView() { + return mDecor; + } + + static private final String FOCUSED_ID_TAG = "android:focusedViewId"; + static private final String VIEWS_TAG = "android:views"; + static private final String PANELS_TAG = "android:Panels"; + static private final String ACTION_BAR_TAG = "android:ActionBar"; + + /** {@inheritDoc} */ + @Override + public Bundle saveHierarchyState() { + Bundle outState = new Bundle(); + if (mContentParent == null) { + return outState; + } + + SparseArray<Parcelable> states = new SparseArray<Parcelable>(); + mContentParent.saveHierarchyState(states); + outState.putSparseParcelableArray(VIEWS_TAG, states); + + // save the focused view id + View focusedView = mContentParent.findFocus(); + if (focusedView != null) { + if (focusedView.getId() != View.NO_ID) { + outState.putInt(FOCUSED_ID_TAG, focusedView.getId()); + } else { + if (false) { + Log.d(TAG, "couldn't save which view has focus because the focused view " + + focusedView + " has no id."); + } + } + } + + // save the panels + SparseArray<Parcelable> panelStates = new SparseArray<Parcelable>(); + savePanelState(panelStates); + if (panelStates.size() > 0) { + outState.putSparseParcelableArray(PANELS_TAG, panelStates); + } + + if (mDecorContentParent != null) { + SparseArray<Parcelable> actionBarStates = new SparseArray<Parcelable>(); + mDecorContentParent.saveToolbarHierarchyState(actionBarStates); + outState.putSparseParcelableArray(ACTION_BAR_TAG, actionBarStates); + } + + return outState; + } + + /** {@inheritDoc} */ + @Override + public void restoreHierarchyState(Bundle savedInstanceState) { + if (mContentParent == null) { + return; + } + + SparseArray<Parcelable> savedStates + = savedInstanceState.getSparseParcelableArray(VIEWS_TAG); + if (savedStates != null) { + mContentParent.restoreHierarchyState(savedStates); + } + + // restore the focused view + int focusedViewId = savedInstanceState.getInt(FOCUSED_ID_TAG, View.NO_ID); + if (focusedViewId != View.NO_ID) { + View needsFocus = mContentParent.findViewById(focusedViewId); + if (needsFocus != null) { + needsFocus.requestFocus(); + } else { + Log.w(TAG, + "Previously focused view reported id " + focusedViewId + + " during save, but can't be found during restore."); + } + } + + // restore the panels + SparseArray<Parcelable> panelStates = savedInstanceState.getSparseParcelableArray(PANELS_TAG); + if (panelStates != null) { + restorePanelState(panelStates); + } + + if (mDecorContentParent != null) { + SparseArray<Parcelable> actionBarStates = + savedInstanceState.getSparseParcelableArray(ACTION_BAR_TAG); + if (actionBarStates != null) { + doPendingInvalidatePanelMenu(); + mDecorContentParent.restoreToolbarHierarchyState(actionBarStates); + } else { + Log.w(TAG, "Missing saved instance states for action bar views! " + + "State will not be restored."); + } + } + } + + /** + * Invoked when the panels should freeze their state. + * + * @param icicles Save state into this. This is usually indexed by the + * featureId. This will be given to {@link #restorePanelState} in the + * future. + */ + private void savePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState[] panels = mPanels; + if (panels == null) { + return; + } + + for (int curFeatureId = panels.length - 1; curFeatureId >= 0; curFeatureId--) { + if (panels[curFeatureId] != null) { + icicles.put(curFeatureId, panels[curFeatureId].onSaveInstanceState()); + } + } + } + + /** + * Invoked when the panels should thaw their state from a previously frozen state. + * + * @param icicles The state saved by {@link #savePanelState} that needs to be thawed. + */ + private void restorePanelState(SparseArray<Parcelable> icicles) { + PanelFeatureState st; + int curFeatureId; + for (int i = icicles.size() - 1; i >= 0; i--) { + curFeatureId = icicles.keyAt(i); + st = getPanelState(curFeatureId, false /* required */); + if (st == null) { + // The panel must not have been required, and is currently not around, skip it + continue; + } + + st.onRestoreInstanceState(icicles.get(curFeatureId)); + invalidatePanelMenu(curFeatureId); + } + + /* + * Implementation note: call openPanelsAfterRestore later to actually open the + * restored panels. + */ + } + + /** + * Opens the panels that have had their state restored. This should be + * called sometime after {@link #restorePanelState} when it is safe to add + * to the window manager. + */ + private void openPanelsAfterRestore() { + PanelFeatureState[] panels = mPanels; + + if (panels == null) { + return; + } + + PanelFeatureState st; + for (int i = panels.length - 1; i >= 0; i--) { + st = panels[i]; + // We restore the panel if it was last open; we skip it if it + // now is open, to avoid a race condition if the user immediately + // opens it when we are resuming. + if (st != null) { + st.applyFrozenState(); + if (!st.isOpen && st.wasLastOpen) { + st.isInExpandedMode = st.wasLastExpanded; + openPanel(st, null); + } + } + } + } + + private class PanelMenuPresenterCallback implements MenuPresenter.Callback { + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + final Menu parentMenu = menu.getRootMenu(); + final boolean isSubMenu = parentMenu != menu; + final PanelFeatureState panel = findMenuPanel(isSubMenu ? parentMenu : menu); + if (panel != null) { + if (isSubMenu) { + callOnPanelClosed(panel.featureId, panel, parentMenu); + closePanel(panel, true); + } else { + // Close the panel and only do the callback if the menu is being + // closed completely, not if opening a sub menu + closePanel(panel, allMenusAreClosing); + } + } + } + + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null && hasFeature(FEATURE_ACTION_BAR)) { + Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu); + } + } + + return true; + } + } + + private final class ActionMenuPresenterCallback implements MenuPresenter.Callback { + @Override + public boolean onOpenSubMenu(MenuBuilder subMenu) { + Callback cb = getCallback(); + if (cb != null) { + cb.onMenuOpened(FEATURE_ACTION_BAR, subMenu); + return true; + } + return false; + } + + @Override + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + checkCloseActionMenu(menu); + } + } + + private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { + + /* package */int mDefaultOpacity = PixelFormat.OPAQUE; + + /** The feature ID of the panel, or -1 if this is the application's DecorView */ + private final int mFeatureId; + + private final Rect mDrawingBounds = new Rect(); + + private final Rect mBackgroundPadding = new Rect(); + + private final Rect mFramePadding = new Rect(); + + private final Rect mFrameOffsets = new Rect(); + + private boolean mChanging; + + private Drawable mMenuBackground; + private boolean mWatchingForMenu; + private int mDownY; + + private ActionMode mPrimaryActionMode; + private ActionMode mFloatingActionMode; + private ActionBarContextView mPrimaryActionModeView; + private PopupWindow mPrimaryActionModePopup; + private Runnable mShowPrimaryActionModePopup; + private OnPreDrawListener mFloatingToolbarPreDrawListener; + private View mFloatingActionModeOriginatingView; + private FloatingToolbar mFloatingToolbar; + + // View added at runtime to draw under the status bar area + private View mStatusGuard; + // View added at runtime to draw under the navigation bar area + private View mNavigationGuard; + + private final ColorViewState mStatusColorViewState = new ColorViewState( + SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS, + Gravity.TOP, + STATUS_BAR_BACKGROUND_TRANSITION_NAME, + com.android.internal.R.id.statusBarBackground, + FLAG_FULLSCREEN); + private final ColorViewState mNavigationColorViewState = new ColorViewState( + SYSTEM_UI_FLAG_HIDE_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION, + Gravity.BOTTOM, + NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME, + com.android.internal.R.id.navigationBarBackground, + 0 /* hideWindowFlag */); + + private final Interpolator mShowInterpolator; + private final Interpolator mHideInterpolator; + private final int mBarEnterExitDuration; + + private final BackgroundFallback mBackgroundFallback = new BackgroundFallback(); + + private int mLastTopInset = 0; + private int mLastBottomInset = 0; + private int mLastRightInset = 0; + private boolean mLastHasTopStableInset = false; + private boolean mLastHasBottomStableInset = false; + private int mLastWindowFlags = 0; + + private int mRootScrollY = 0; + + public DecorView(Context context, int featureId) { + super(context); + mFeatureId = featureId; + + mShowInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.linear_out_slow_in); + mHideInterpolator = AnimationUtils.loadInterpolator(context, + android.R.interpolator.fast_out_linear_in); + + mBarEnterExitDuration = context.getResources().getInteger( + R.integer.dock_enter_exit_duration); + } + + public void setBackgroundFallback(int resId) { + mBackgroundFallback.setDrawable(resId != 0 ? getContext().getDrawable(resId) : null); + setWillNotDraw(getBackground() == null && !mBackgroundFallback.hasFallback()); + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + mBackgroundFallback.draw(mContentRoot, c, mContentParent); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + final int keyCode = event.getKeyCode(); + final int action = event.getAction(); + final boolean isDown = action == KeyEvent.ACTION_DOWN; + + if (isDown && (event.getRepeatCount() == 0)) { + // First handle chording of panel key: if a panel key is held + // but not released, try to execute a shortcut in it. + if ((mPanelChordingKey > 0) && (mPanelChordingKey != keyCode)) { + boolean handled = dispatchKeyShortcutEvent(event); + if (handled) { + return true; + } + } + + // If a panel is open, perform a shortcut on it without the + // chorded panel key + if ((mPreparedPanel != null) && mPreparedPanel.isOpen) { + if (performPanelShortcut(mPreparedPanel, keyCode, event, 0)) { + return true; + } + } + } + + if (!isDestroyed()) { + final Callback cb = getCallback(); + final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event) + : super.dispatchKeyEvent(event); + if (handled) { + return true; + } + } + + return isDown ? PhoneWindow.this.onKeyDown(mFeatureId, event.getKeyCode(), event) + : PhoneWindow.this.onKeyUp(mFeatureId, event.getKeyCode(), event); + } + + @Override + public boolean dispatchKeyShortcutEvent(KeyEvent ev) { + // If the panel is already prepared, then perform the shortcut using it. + boolean handled; + if (mPreparedPanel != null) { + handled = performPanelShortcut(mPreparedPanel, ev.getKeyCode(), ev, + Menu.FLAG_PERFORM_NO_CLOSE); + if (handled) { + if (mPreparedPanel != null) { + mPreparedPanel.isHandled = true; + } + return true; + } + } + + // Shortcut not handled by the panel. Dispatch to the view hierarchy. + final Callback cb = getCallback(); + handled = cb != null && !isDestroyed() && mFeatureId < 0 + ? cb.dispatchKeyShortcutEvent(ev) : super.dispatchKeyShortcutEvent(ev); + if (handled) { + return true; + } + + // If the panel is not prepared, then we may be trying to handle a shortcut key + // combination such as Control+C. Temporarily prepare the panel then mark it + // unprepared again when finished to ensure that the panel will again be prepared + // the next time it is shown for real. + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && mPreparedPanel == null) { + preparePanel(st, ev); + handled = performPanelShortcut(st, ev.getKeyCode(), ev, + Menu.FLAG_PERFORM_NO_CLOSE); + st.isPrepared = false; + if (handled) { + return true; + } + } + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) + : super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchTrackballEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTrackballEvent(ev) + : super.dispatchTrackballEvent(ev); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent ev) { + final Callback cb = getCallback(); + return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchGenericMotionEvent(ev) + : super.dispatchGenericMotionEvent(ev); + } + + public boolean superDispatchKeyEvent(KeyEvent event) { + // Give priority to closing action modes if applicable. + if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) { + final int action = event.getAction(); + // Back cancels action modes first. + if (mPrimaryActionMode != null) { + if (action == KeyEvent.ACTION_UP) { + mPrimaryActionMode.finish(); + } + return true; + } + } + + return super.dispatchKeyEvent(event); + } + + public boolean superDispatchKeyShortcutEvent(KeyEvent event) { + return super.dispatchKeyShortcutEvent(event); + } + + public boolean superDispatchTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + public boolean superDispatchTrackballEvent(MotionEvent event) { + return super.dispatchTrackballEvent(event); + } + + public boolean superDispatchGenericMotionEvent(MotionEvent event) { + return super.dispatchGenericMotionEvent(event); + } + + @Override + public WindowInsets dispatchApplyWindowInsets(WindowInsets insets) { + if (mOutsetBottomPx != 0) { + WindowInsets newInsets = insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight(), mOutsetBottomPx); + return super.dispatchApplyWindowInsets(newInsets); + } else { + return super.dispatchApplyWindowInsets(insets); + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + return onInterceptTouchEvent(event); + } + + private boolean isOutOfBounds(int x, int y) { + return x < -5 || y < -5 || x > (getWidth() + 5) + || y > (getHeight() + 5); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent event) { + int action = event.getAction(); + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + int x = (int)event.getX(); + int y = (int)event.getY(); + if (isOutOfBounds(x, y)) { + closePanel(mFeatureId); + return true; + } + } + } + + if (!SWEEP_OPEN_MENU) { + return false; + } + + if (mFeatureId >= 0) { + if (action == MotionEvent.ACTION_DOWN) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + mDownY = (int) event.getY(); + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y > (mDownY+30)) { + Log.i(TAG, "Closing!"); + closePanel(mFeatureId); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + //Log.i(TAG, "Intercept: action=" + action + " y=" + event.getY() + // + " (in " + getHeight() + ")"); + + if (action == MotionEvent.ACTION_DOWN) { + int y = (int)event.getY(); + if (y >= (getHeight()-5) && !hasChildren()) { + Log.i(TAG, "Watchiing!"); + mWatchingForMenu = true; + } + return false; + } + + if (!mWatchingForMenu) { + return false; + } + + int y = (int)event.getY(); + if (action == MotionEvent.ACTION_MOVE) { + if (y < (getHeight()-30)) { + Log.i(TAG, "Opening!"); + openPanel(FEATURE_OPTIONS_PANEL, new KeyEvent( + KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)); + mWatchingForMenu = false; + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + mWatchingForMenu = false; + } + + return false; + } + + @Override + public void sendAccessibilityEvent(int eventType) { + if (!AccessibilityManager.getInstance(mContext).isEnabled()) { + return; + } + + // if we are showing a feature that should be announced and one child + // make this child the event source since this is the feature itself + // otherwise the callback will take over and announce its client + if ((mFeatureId == FEATURE_OPTIONS_PANEL || + mFeatureId == FEATURE_CONTEXT_MENU || + mFeatureId == FEATURE_PROGRESS || + mFeatureId == FEATURE_INDETERMINATE_PROGRESS) + && getChildCount() == 1) { + getChildAt(0).sendAccessibilityEvent(eventType); + } else { + super.sendAccessibilityEvent(eventType); + } + } + + @Override + public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) { + final Callback cb = getCallback(); + if (cb != null && !isDestroyed()) { + if (cb.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + return super.dispatchPopulateAccessibilityEventInternal(event); + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + if (changed) { + final Rect drawingBounds = mDrawingBounds; + getDrawingRect(drawingBounds); + + Drawable fg = getForeground(); + if (fg != null) { + final Rect frameOffsets = mFrameOffsets; + drawingBounds.left += frameOffsets.left; + drawingBounds.top += frameOffsets.top; + drawingBounds.right -= frameOffsets.right; + drawingBounds.bottom -= frameOffsets.bottom; + fg.setBounds(drawingBounds); + final Rect framePadding = mFramePadding; + drawingBounds.left += framePadding.left - frameOffsets.left; + drawingBounds.top += framePadding.top - frameOffsets.top; + drawingBounds.right -= framePadding.right - frameOffsets.right; + drawingBounds.bottom -= framePadding.bottom - frameOffsets.bottom; + } + + Drawable bg = getBackground(); + if (bg != null) { + bg.setBounds(drawingBounds); + } + + if (SWEEP_OPEN_MENU) { + if (mMenuBackground == null && mFeatureId < 0 + && getAttributes().height + == WindowManager.LayoutParams.MATCH_PARENT) { + mMenuBackground = getContext().getDrawable( + R.drawable.menu_background); + } + if (mMenuBackground != null) { + mMenuBackground.setBounds(drawingBounds.left, + drawingBounds.bottom-6, drawingBounds.right, + drawingBounds.bottom+20); + } + } + } + return changed; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + final boolean isPortrait = metrics.widthPixels < metrics.heightPixels; + + final int widthMode = getMode(widthMeasureSpec); + final int heightMode = getMode(heightMeasureSpec); + + boolean fixedWidth = false; + if (widthMode == AT_MOST) { + final TypedValue tvw = isPortrait ? mFixedWidthMinor : mFixedWidthMajor; + if (tvw != null && tvw.type != TypedValue.TYPE_NULL) { + final int w; + if (tvw.type == TypedValue.TYPE_DIMENSION) { + w = (int) tvw.getDimension(metrics); + } else if (tvw.type == TypedValue.TYPE_FRACTION) { + w = (int) tvw.getFraction(metrics.widthPixels, metrics.widthPixels); + } else { + w = 0; + } + + if (w > 0) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + widthMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(w, widthSize), EXACTLY); + fixedWidth = true; + } + } + } + + if (heightMode == AT_MOST) { + final TypedValue tvh = isPortrait ? mFixedHeightMajor : mFixedHeightMinor; + if (tvh != null && tvh.type != TypedValue.TYPE_NULL) { + final int h; + if (tvh.type == TypedValue.TYPE_DIMENSION) { + h = (int) tvh.getDimension(metrics); + } else if (tvh.type == TypedValue.TYPE_FRACTION) { + h = (int) tvh.getFraction(metrics.heightPixels, metrics.heightPixels); + } else { + h = 0; + } + if (h > 0) { + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec( + Math.min(h, heightSize), EXACTLY); + } + } + } + + if (mOutsetBottomPx != 0) { + int mode = MeasureSpec.getMode(heightMeasureSpec); + if (mode != MeasureSpec.UNSPECIFIED) { + int height = MeasureSpec.getSize(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.makeMeasureSpec(height + mOutsetBottomPx, mode); + } + } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + int width = getMeasuredWidth(); + boolean measure = false; + + widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, EXACTLY); + + if (!fixedWidth && widthMode == AT_MOST) { + final TypedValue tv = isPortrait ? mMinWidthMinor : mMinWidthMajor; + if (tv.type != TypedValue.TYPE_NULL) { + final int min; + if (tv.type == TypedValue.TYPE_DIMENSION) { + min = (int)tv.getDimension(metrics); + } else if (tv.type == TypedValue.TYPE_FRACTION) { + min = (int)tv.getFraction(metrics.widthPixels, metrics.widthPixels); + } else { + min = 0; + } + + if (width < min) { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(min, EXACTLY); + measure = true; + } + } + } + + // TODO: Support height? + + if (measure) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + + if (mMenuBackground != null) { + mMenuBackground.draw(canvas); + } + } + + + @Override + public boolean showContextMenuForChild(View originalView) { + // Reuse the context menu builder + if (mContextMenu == null) { + mContextMenu = new ContextMenuBuilder(getContext()); + mContextMenu.setCallback(mContextMenuCallback); + } else { + mContextMenu.clearAll(); + } + + final MenuDialogHelper helper = mContextMenu.show(originalView, + originalView.getWindowToken()); + if (helper != null) { + helper.setPresenterCallback(mContextMenuCallback); + } else if (mContextMenuHelper != null) { + // No menu to show, but if we have a menu currently showing it just became blank. + // Close it. + mContextMenuHelper.dismiss(); + } + mContextMenuHelper = helper; + return helper != null; + } + + @Override + public ActionMode startActionModeForChild(View originalView, + ActionMode.Callback callback) { + return startActionModeForChild(originalView, callback, ActionMode.TYPE_PRIMARY); + } + + @Override + public ActionMode startActionModeForChild( + View child, ActionMode.Callback callback, int type) { + return startActionMode(child, callback, type); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback) { + return startActionMode(callback, ActionMode.TYPE_PRIMARY); + } + + @Override + public ActionMode startActionMode(ActionMode.Callback callback, int type) { + return startActionMode(this, callback, type); + } + + private ActionMode startActionMode( + View originatingView, ActionMode.Callback callback, int type) { + ActionMode.Callback2 wrappedCallback = new ActionModeCallback2Wrapper(callback); + ActionMode mode = null; + if (getCallback() != null && !isDestroyed()) { + try { + mode = getCallback().onWindowStartingActionMode(wrappedCallback, type); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + if (mode != null) { + if (mode.getType() == ActionMode.TYPE_PRIMARY) { + cleanupPrimaryActionMode(); + mPrimaryActionMode = mode; + } else if (mode.getType() == ActionMode.TYPE_FLOATING) { + if (mFloatingActionMode != null) { + mFloatingActionMode.finish(); + } + mFloatingActionMode = mode; + } + } else { + mode = createActionMode(type, wrappedCallback, originatingView); + if (mode != null && wrappedCallback.onCreateActionMode(mode, mode.getMenu())) { + setHandledActionMode(mode); + } else { + mode = null; + } + } + if (mode != null && getCallback() != null && !isDestroyed()) { + try { + getCallback().onActionModeStarted(mode); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + return mode; + } + + private void cleanupPrimaryActionMode() { + if (mPrimaryActionMode != null) { + mPrimaryActionMode.finish(); + mPrimaryActionMode = null; + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.killMode(); + } + } + + private void cleanupFloatingActionModeViews() { + if (mFloatingToolbar != null) { + mFloatingToolbar.dismiss(); + mFloatingToolbar = null; + } + if (mFloatingActionModeOriginatingView != null) { + if (mFloatingToolbarPreDrawListener != null) { + mFloatingActionModeOriginatingView.getViewTreeObserver() + .removeOnPreDrawListener(mFloatingToolbarPreDrawListener); + mFloatingToolbarPreDrawListener = null; + } + mFloatingActionModeOriginatingView = null; + } + } + + public void startChanging() { + mChanging = true; + } + + public void finishChanging() { + mChanging = false; + drawableChanged(); + } + + public void setWindowBackground(Drawable drawable) { + if (getBackground() != drawable) { + setBackgroundDrawable(drawable); + if (drawable != null) { + drawable.getPadding(mBackgroundPadding); + } else { + mBackgroundPadding.setEmpty(); + } + drawableChanged(); + } + } + + @Override + public void setBackgroundDrawable(Drawable d) { + super.setBackgroundDrawable(d); + if (getWindowToken() != null) { + updateWindowResizeState(); + } + } + + public void setWindowFrame(Drawable drawable) { + if (getForeground() != drawable) { + setForeground(drawable); + if (drawable != null) { + drawable.getPadding(mFramePadding); + } else { + mFramePadding.setEmpty(); + } + drawableChanged(); + } + } + + @Override + public void onWindowSystemUiVisibilityChanged(int visible) { + updateColorViews(null /* insets */, true /* animate */); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mFrameOffsets.set(insets.getSystemWindowInsets()); + insets = updateColorViews(insets, true /* animate */); + insets = updateStatusGuard(insets); + updateNavigationGuard(insets); + if (getForeground() != null) { + drawableChanged(); + } + return insets; + } + + @Override + public boolean isTransitionGroup() { + return false; + } + + private WindowInsets updateColorViews(WindowInsets insets, boolean animate) { + WindowManager.LayoutParams attrs = getAttributes(); + int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility(); + + if (!mIsFloating && ActivityManager.isHighEndGfx()) { + boolean disallowAnimate = !isLaidOut(); + disallowAnimate |= ((mLastWindowFlags ^ attrs.flags) + & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + mLastWindowFlags = attrs.flags; + + if (insets != null) { + mLastTopInset = Math.min(insets.getStableInsetTop(), + insets.getSystemWindowInsetTop()); + mLastBottomInset = Math.min(insets.getStableInsetBottom(), + insets.getSystemWindowInsetBottom()); + mLastRightInset = Math.min(insets.getStableInsetRight(), + insets.getSystemWindowInsetRight()); + + // Don't animate if the presence of stable insets has changed, because that + // indicates that the window was either just added and received them for the + // first time, or the window size or position has changed. + boolean hasTopStableInset = insets.getStableInsetTop() != 0; + disallowAnimate |= (hasTopStableInset != mLastHasTopStableInset); + mLastHasTopStableInset = hasTopStableInset; + + boolean hasBottomStableInset = insets.getStableInsetBottom() != 0; + disallowAnimate |= (hasBottomStableInset != mLastHasBottomStableInset); + mLastHasBottomStableInset = hasBottomStableInset; + } + + updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor, + mLastTopInset, animate && !disallowAnimate); + updateColorViewInt(mNavigationColorViewState, sysUiVisibility, mNavigationBarColor, + mLastBottomInset, animate && !disallowAnimate); + } + + // When we expand the window with FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, we still need + // to ensure that the rest of the view hierarchy doesn't notice it, unless they've + // explicitly asked for it. + + boolean consumingNavBar = + (attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0 + && (sysUiVisibility & SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION) == 0 + && (sysUiVisibility & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0; + + int consumedRight = consumingNavBar ? mLastRightInset : 0; + int consumedBottom = consumingNavBar ? mLastBottomInset : 0; + + if (mContentRoot != null + && mContentRoot.getLayoutParams() instanceof MarginLayoutParams) { + MarginLayoutParams lp = (MarginLayoutParams) mContentRoot.getLayoutParams(); + if (lp.rightMargin != consumedRight || lp.bottomMargin != consumedBottom) { + lp.rightMargin = consumedRight; + lp.bottomMargin = consumedBottom; + mContentRoot.setLayoutParams(lp); + + if (insets == null) { + // The insets have changed, but we're not currently in the process + // of dispatching them. + requestApplyInsets(); + } + } + if (insets != null) { + insets = insets.replaceSystemWindowInsets( + insets.getSystemWindowInsetLeft(), + insets.getSystemWindowInsetTop(), + insets.getSystemWindowInsetRight() - consumedRight, + insets.getSystemWindowInsetBottom() - consumedBottom); + } + } + + if (insets != null) { + insets = insets.consumeStableInsets(); + } + return insets; + } + + private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color, + int height, boolean animate) { + boolean show = height > 0 && (sysUiVis & state.systemUiHideFlag) == 0 + && (getAttributes().flags & state.hideWindowFlag) == 0 + && (getAttributes().flags & state.translucentFlag) == 0 + && (color & Color.BLACK) != 0 + && (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0; + + boolean visibilityChanged = false; + View view = state.view; + + if (view == null) { + if (show) { + state.view = view = new View(mContext); + view.setBackgroundColor(color); + view.setTransitionName(state.transitionName); + view.setId(state.id); + visibilityChanged = true; + view.setVisibility(INVISIBLE); + state.targetVisibility = VISIBLE; + + addView(view, new LayoutParams(LayoutParams.MATCH_PARENT, height, + Gravity.START | state.verticalGravity)); + updateColorViewTranslations(); + } + } else { + int vis = show ? VISIBLE : INVISIBLE; + visibilityChanged = state.targetVisibility != vis; + state.targetVisibility = vis; + if (show) { + LayoutParams lp = (LayoutParams) view.getLayoutParams(); + if (lp.height != height) { + lp.height = height; + view.setLayoutParams(lp); + } + view.setBackgroundColor(color); + } + } + if (visibilityChanged) { + view.animate().cancel(); + if (animate) { + if (show) { + if (view.getVisibility() != VISIBLE) { + view.setVisibility(VISIBLE); + view.setAlpha(0.0f); + } + view.animate().alpha(1.0f).setInterpolator(mShowInterpolator). + setDuration(mBarEnterExitDuration); + } else { + view.animate().alpha(0.0f).setInterpolator(mHideInterpolator) + .setDuration(mBarEnterExitDuration) + .withEndAction(new Runnable() { + @Override + public void run() { + state.view.setAlpha(1.0f); + state.view.setVisibility(INVISIBLE); + } + }); + } + } else { + view.setAlpha(1.0f); + view.setVisibility(show ? VISIBLE : INVISIBLE); + } + } + } + + private void updateColorViewTranslations() { + // Put the color views back in place when they get moved off the screen + // due to the the ViewRootImpl panning. + int rootScrollY = mRootScrollY; + if (mStatusColorViewState.view != null) { + mStatusColorViewState.view.setTranslationY(rootScrollY > 0 ? rootScrollY : 0); + } + if (mNavigationColorViewState.view != null) { + mNavigationColorViewState.view.setTranslationY(rootScrollY < 0 ? rootScrollY : 0); + } + } + + private WindowInsets updateStatusGuard(WindowInsets insets) { + boolean showStatusGuard = false; + // Show the status guard when the non-overlay contextual action bar is showing + if (mPrimaryActionModeView != null) { + if (mPrimaryActionModeView.getLayoutParams() instanceof MarginLayoutParams) { + // Insets are magic! + final MarginLayoutParams mlp = (MarginLayoutParams) + mPrimaryActionModeView.getLayoutParams(); + boolean mlpChanged = false; + if (mPrimaryActionModeView.isShown()) { + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect rect = mTempRect; + + // If the parent doesn't consume the insets, manually + // apply the default system window insets. + mContentParent.computeSystemWindowInsets(insets, rect); + final int newMargin = rect.top == 0 ? insets.getSystemWindowInsetTop() : 0; + if (mlp.topMargin != newMargin) { + mlpChanged = true; + mlp.topMargin = insets.getSystemWindowInsetTop(); + + if (mStatusGuard == null) { + mStatusGuard = new View(mContext); + mStatusGuard.setBackgroundColor(mContext.getColor( + R.color.input_method_navigation_guard)); + addView(mStatusGuard, indexOfChild(mStatusColorViewState.view), + new LayoutParams(LayoutParams.MATCH_PARENT, + mlp.topMargin, Gravity.START | Gravity.TOP)); + } else { + final LayoutParams lp = (LayoutParams) + mStatusGuard.getLayoutParams(); + if (lp.height != mlp.topMargin) { + lp.height = mlp.topMargin; + mStatusGuard.setLayoutParams(lp); + } + } + } + + // The action mode's theme may differ from the app, so + // always show the status guard above it if we have one. + showStatusGuard = mStatusGuard != null; + + // We only need to consume the insets if the action + // mode is overlaid on the app content (e.g. it's + // sitting in a FrameLayout, see + // screen_simple_overlay_action_mode.xml). + final boolean nonOverlay = (getLocalFeatures() + & (1 << FEATURE_ACTION_MODE_OVERLAY)) == 0; + insets = insets.consumeSystemWindowInsets( + false, nonOverlay && showStatusGuard /* top */, false, false); + } else { + // reset top margin + if (mlp.topMargin != 0) { + mlpChanged = true; + mlp.topMargin = 0; + } + } + if (mlpChanged) { + mPrimaryActionModeView.setLayoutParams(mlp); + } + } + } + if (mStatusGuard != null) { + mStatusGuard.setVisibility(showStatusGuard ? View.VISIBLE : View.GONE); + } + return insets; + } + + private void updateNavigationGuard(WindowInsets insets) { + // IMEs lay out below the nav bar, but the content view must not (for back compat) + if (getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD) { + // prevent the content view from including the nav bar height + if (mContentParent != null) { + if (mContentParent.getLayoutParams() instanceof MarginLayoutParams) { + MarginLayoutParams mlp = + (MarginLayoutParams) mContentParent.getLayoutParams(); + mlp.bottomMargin = insets.getSystemWindowInsetBottom(); + mContentParent.setLayoutParams(mlp); + } + } + // position the navigation guard view, creating it if necessary + if (mNavigationGuard == null) { + mNavigationGuard = new View(mContext); + mNavigationGuard.setBackgroundColor(mContext.getColor( + R.color.input_method_navigation_guard)); + addView(mNavigationGuard, indexOfChild(mNavigationColorViewState.view), + new LayoutParams(LayoutParams.MATCH_PARENT, + insets.getSystemWindowInsetBottom(), + Gravity.START | Gravity.BOTTOM)); + } else { + LayoutParams lp = (LayoutParams) mNavigationGuard.getLayoutParams(); + lp.height = insets.getSystemWindowInsetBottom(); + mNavigationGuard.setLayoutParams(lp); + } + } + } + + private void drawableChanged() { + if (mChanging) { + return; + } + + setPadding(mFramePadding.left + mBackgroundPadding.left, mFramePadding.top + + mBackgroundPadding.top, mFramePadding.right + mBackgroundPadding.right, + mFramePadding.bottom + mBackgroundPadding.bottom); + requestLayout(); + invalidate(); + + int opacity = PixelFormat.OPAQUE; + // Note: if there is no background, we will assume opaque. The + // common case seems to be that an application sets there to be + // no background so it can draw everything itself. For that, + // we would like to assume OPAQUE and let the app force it to + // the slower TRANSLUCENT mode if that is really what it wants. + Drawable bg = getBackground(); + Drawable fg = getForeground(); + if (bg != null) { + if (fg == null) { + opacity = bg.getOpacity(); + } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0 + && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) { + // If the frame padding is zero, then we can be opaque + // if either the frame -or- the background is opaque. + int fop = fg.getOpacity(); + int bop = bg.getOpacity(); + if (false) + Log.v(TAG, "Background opacity: " + bop + ", Frame opacity: " + fop); + if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) { + opacity = PixelFormat.OPAQUE; + } else if (fop == PixelFormat.UNKNOWN) { + opacity = bop; + } else if (bop == PixelFormat.UNKNOWN) { + opacity = fop; + } else { + opacity = Drawable.resolveOpacity(fop, bop); + } + } else { + // For now we have to assume translucent if there is a + // frame with padding... there is no way to tell if the + // frame and background together will draw all pixels. + if (false) + Log.v(TAG, "Padding: " + mFramePadding); + opacity = PixelFormat.TRANSLUCENT; + } + } + + if (false) + Log.v(TAG, "Background: " + bg + ", Frame: " + fg); + if (false) + Log.v(TAG, "Selected default opacity: " + opacity); + + mDefaultOpacity = opacity; + if (mFeatureId < 0) { + setDefaultWindowFormat(opacity); + } + } + + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + + // If the user is chording a menu shortcut, release the chord since + // this window lost focus + if (hasFeature(FEATURE_OPTIONS_PANEL) && !hasWindowFocus && mPanelChordingKey != 0) { + closePanel(FEATURE_OPTIONS_PANEL); + } + + final Callback cb = getCallback(); + if (cb != null && !isDestroyed() && mFeatureId < 0) { + cb.onWindowFocusChanged(hasWindowFocus); + } + + if (mFloatingToolbar != null) { + if (hasWindowFocus) { + mFloatingToolbar.show(); + } else { + mFloatingToolbar.dismiss(); + } + } + } + + void updateWindowResizeState() { + Drawable bg = getBackground(); + hackTurnOffWindowResizeAnim(bg == null || bg.getOpacity() + != PixelFormat.OPAQUE); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + updateWindowResizeState(); + + final Callback cb = getCallback(); + if (cb != null && !isDestroyed() && mFeatureId < 0) { + cb.onAttachedToWindow(); + } + + if (mFeatureId == -1) { + /* + * The main window has been attached, try to restore any panels + * that may have been open before. This is called in cases where + * an activity is being killed for configuration change and the + * menu was open. When the activity is recreated, the menu + * should be shown again. + */ + openPanelsAfterRestore(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + final Callback cb = getCallback(); + if (cb != null && mFeatureId < 0) { + cb.onDetachedFromWindow(); + } + + if (mDecorContentParent != null) { + mDecorContentParent.dismissPopups(); + } + + if (mPrimaryActionModePopup != null) { + removeCallbacks(mShowPrimaryActionModePopup); + if (mPrimaryActionModePopup.isShowing()) { + mPrimaryActionModePopup.dismiss(); + } + mPrimaryActionModePopup = null; + } + if (mFloatingToolbar != null) { + mFloatingToolbar.dismiss(); + mFloatingToolbar = null; + } + + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (st != null && st.menu != null && mFeatureId < 0) { + st.menu.close(); + } + } + + @Override + public void onCloseSystemDialogs(String reason) { + if (mFeatureId >= 0) { + closeAllPanels(); + } + } + + public android.view.SurfaceHolder.Callback2 willYouTakeTheSurface() { + return mFeatureId < 0 ? mTakeSurfaceCallback : null; + } + + public InputQueue.Callback willYouTakeTheInputQueue() { + return mFeatureId < 0 ? mTakeInputQueueCallback : null; + } + + public void setSurfaceType(int type) { + PhoneWindow.this.setType(type); + } + + public void setSurfaceFormat(int format) { + PhoneWindow.this.setFormat(format); + } + + public void setSurfaceKeepScreenOn(boolean keepOn) { + if (keepOn) PhoneWindow.this.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + else PhoneWindow.this.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + } + + @Override + public void onRootViewScrollYChanged(int rootScrollY) { + mRootScrollY = rootScrollY; + updateColorViewTranslations(); + } + + private ActionMode createActionMode( + int type, ActionMode.Callback2 callback, View originatingView) { + switch (type) { + case ActionMode.TYPE_PRIMARY: + default: + return createStandaloneActionMode(callback); + case ActionMode.TYPE_FLOATING: + return createFloatingActionMode(originatingView, callback); + } + } + + private void setHandledActionMode(ActionMode mode) { + if (mode.getType() == ActionMode.TYPE_PRIMARY) { + setHandledPrimaryActionMode(mode); + } else if (mode.getType() == ActionMode.TYPE_FLOATING) { + setHandledFloatingActionMode(mode); + } + } + + private ActionMode createStandaloneActionMode(ActionMode.Callback callback) { + cleanupPrimaryActionMode(); + if (mPrimaryActionModeView == null) { + if (isFloating()) { + // Use the action bar theme. + final TypedValue outValue = new TypedValue(); + final Theme baseTheme = mContext.getTheme(); + baseTheme.resolveAttribute(R.attr.actionBarTheme, outValue, true); + + final Context actionBarContext; + if (outValue.resourceId != 0) { + final Theme actionBarTheme = mContext.getResources().newTheme(); + actionBarTheme.setTo(baseTheme); + actionBarTheme.applyStyle(outValue.resourceId, true); + + actionBarContext = new ContextThemeWrapper(mContext, 0); + actionBarContext.getTheme().setTo(actionBarTheme); + } else { + actionBarContext = mContext; + } + + mPrimaryActionModeView = new ActionBarContextView(actionBarContext); + mPrimaryActionModePopup = new PopupWindow(actionBarContext, null, + R.attr.actionModePopupWindowStyle); + mPrimaryActionModePopup.setWindowLayoutType( + WindowManager.LayoutParams.TYPE_APPLICATION); + mPrimaryActionModePopup.setContentView(mPrimaryActionModeView); + mPrimaryActionModePopup.setWidth(MATCH_PARENT); + + actionBarContext.getTheme().resolveAttribute( + R.attr.actionBarSize, outValue, true); + final int height = TypedValue.complexToDimensionPixelSize(outValue.data, + actionBarContext.getResources().getDisplayMetrics()); + mPrimaryActionModeView.setContentHeight(height); + mPrimaryActionModePopup.setHeight(WRAP_CONTENT); + mShowPrimaryActionModePopup = new Runnable() { + public void run() { + mPrimaryActionModePopup.showAtLocation( + mPrimaryActionModeView.getApplicationWindowToken(), + Gravity.TOP | Gravity.FILL_HORIZONTAL, 0, 0); + } + }; + } else { + ViewStub stub = (ViewStub) findViewById( + R.id.action_mode_bar_stub); + if (stub != null) { + mPrimaryActionModeView = (ActionBarContextView) stub.inflate(); + } + } + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.killMode(); + ActionMode mode = new StandaloneActionMode( + mPrimaryActionModeView.getContext(), mPrimaryActionModeView, + callback, mPrimaryActionModePopup == null); + return mode; + } + return null; + } + + private void setHandledPrimaryActionMode(ActionMode mode) { + mPrimaryActionMode = mode; + mPrimaryActionMode.invalidate(); + mPrimaryActionModeView.initForMode(mPrimaryActionMode); + mPrimaryActionModeView.setVisibility(View.VISIBLE); + if (mPrimaryActionModePopup != null) { + post(mShowPrimaryActionModePopup); + } + mPrimaryActionModeView.sendAccessibilityEvent( + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + private ActionMode createFloatingActionMode( + View originatingView, ActionMode.Callback2 callback) { + if (mFloatingActionMode != null) { + mFloatingActionMode.finish(); + } + cleanupFloatingActionModeViews(); + mFloatingToolbar = new FloatingToolbar(mContext, PhoneWindow.this); + final FloatingActionMode mode = new FloatingActionMode( + mContext, callback, originatingView, mFloatingToolbar); + mFloatingActionModeOriginatingView = originatingView; + mFloatingToolbarPreDrawListener = + new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + mode.updateViewLocationInWindow(); + return true; + } + }; + return mode; + } + + private void setHandledFloatingActionMode(ActionMode mode) { + mFloatingActionMode = mode; + mFloatingActionMode.invalidate(); + mFloatingToolbar.show(); + mFloatingActionModeOriginatingView.getViewTreeObserver() + .addOnPreDrawListener(mFloatingToolbarPreDrawListener); + } + + /** + * Clears out internal references when the action mode is destroyed. + */ + private class ActionModeCallback2Wrapper extends ActionMode.Callback2 { + private final ActionMode.Callback mWrapped; + + public ActionModeCallback2Wrapper(ActionMode.Callback wrapped) { + mWrapped = wrapped; + } + + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + return mWrapped.onCreateActionMode(mode, menu); + } + + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + requestFitSystemWindows(); + return mWrapped.onPrepareActionMode(mode, menu); + } + + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + return mWrapped.onActionItemClicked(mode, item); + } + + public void onDestroyActionMode(ActionMode mode) { + mWrapped.onDestroyActionMode(mode); + if (mode == mPrimaryActionMode) { + if (mPrimaryActionModePopup != null) { + removeCallbacks(mShowPrimaryActionModePopup); + mPrimaryActionModePopup.dismiss(); + } else if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.setVisibility(GONE); + } + if (mPrimaryActionModeView != null) { + mPrimaryActionModeView.removeAllViews(); + } + mPrimaryActionMode = null; + } else if (mode == mFloatingActionMode) { + cleanupFloatingActionModeViews(); + mFloatingActionMode = null; + } + if (getCallback() != null && !isDestroyed()) { + try { + getCallback().onActionModeFinished(mode); + } catch (AbstractMethodError ame) { + // Older apps might not implement this callback method. + } + } + requestFitSystemWindows(); + } + + @Override + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { + if (mWrapped instanceof ActionMode.Callback2) { + ((ActionMode.Callback2) mWrapped).onGetContentRect(mode, view, outRect); + } else { + super.onGetContentRect(mode, view, outRect); + } + } + } + } + + protected DecorView generateDecor() { + return new DecorView(getContext(), -1); + } + + protected void setFeatureFromAttrs(int featureId, TypedArray attrs, + int drawableAttr, int alphaAttr) { + Drawable d = attrs.getDrawable(drawableAttr); + if (d != null) { + requestFeature(featureId); + setFeatureDefaultDrawable(featureId, d); + } + if ((getFeatures() & (1 << featureId)) != 0) { + int alpha = attrs.getInt(alphaAttr, -1); + if (alpha >= 0) { + setFeatureDrawableAlpha(featureId, alpha); + } + } + } + + protected ViewGroup generateLayout(DecorView decor) { + // Apply data from current theme. + + TypedArray a = getWindowStyle(); + + if (false) { + System.out.println("From style:"); + String s = "Attrs:"; + for (int i = 0; i < R.styleable.Window.length; i++) { + s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "=" + + a.getString(i); + } + System.out.println(s); + } + + mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false); + int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR) + & (~getForcedWindowFlags()); + if (mIsFloating) { + setLayout(WRAP_CONTENT, WRAP_CONTENT); + setFlags(0, flagsToUpdate); + } else { + setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate); + } + + if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { + requestFeature(FEATURE_NO_TITLE); + } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) { + // Don't allow an action bar if there is no title. + requestFeature(FEATURE_ACTION_BAR); + } + + if (a.getBoolean(R.styleable.Window_windowActionBarOverlay, false)) { + requestFeature(FEATURE_ACTION_BAR_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowActionModeOverlay, false)) { + requestFeature(FEATURE_ACTION_MODE_OVERLAY); + } + + if (a.getBoolean(R.styleable.Window_windowSwipeToDismiss, false)) { + requestFeature(FEATURE_SWIPE_TO_DISMISS); + } + + if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentStatus, + false)) { + setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowTranslucentNavigation, + false)) { + setFlags(FLAG_TRANSLUCENT_NAVIGATION, FLAG_TRANSLUCENT_NAVIGATION + & (~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowOverscan, false)) { + setFlags(FLAG_LAYOUT_IN_OVERSCAN, FLAG_LAYOUT_IN_OVERSCAN&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowShowWallpaper, false)) { + setFlags(FLAG_SHOW_WALLPAPER, FLAG_SHOW_WALLPAPER&(~getForcedWindowFlags())); + } + + if (a.getBoolean(R.styleable.Window_windowEnableSplitTouch, + getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB)) { + setFlags(FLAG_SPLIT_TOUCH, FLAG_SPLIT_TOUCH&(~getForcedWindowFlags())); + } + + a.getValue(R.styleable.Window_windowMinWidthMajor, mMinWidthMajor); + a.getValue(R.styleable.Window_windowMinWidthMinor, mMinWidthMinor); + if (a.hasValue(R.styleable.Window_windowFixedWidthMajor)) { + if (mFixedWidthMajor == null) mFixedWidthMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMajor, + mFixedWidthMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedWidthMinor)) { + if (mFixedWidthMinor == null) mFixedWidthMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedWidthMinor, + mFixedWidthMinor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMajor)) { + if (mFixedHeightMajor == null) mFixedHeightMajor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMajor, + mFixedHeightMajor); + } + if (a.hasValue(R.styleable.Window_windowFixedHeightMinor)) { + if (mFixedHeightMinor == null) mFixedHeightMinor = new TypedValue(); + a.getValue(R.styleable.Window_windowFixedHeightMinor, + mFixedHeightMinor); + } + if (a.getBoolean(R.styleable.Window_windowContentTransitions, false)) { + requestFeature(FEATURE_CONTENT_TRANSITIONS); + } + if (a.getBoolean(R.styleable.Window_windowActivityTransitions, false)) { + requestFeature(FEATURE_ACTIVITY_TRANSITIONS); + } + + final WindowManager windowService = (WindowManager) getContext().getSystemService( + Context.WINDOW_SERVICE); + if (windowService != null) { + final Display display = windowService.getDefaultDisplay(); + final boolean shouldUseBottomOutset = + display.getDisplayId() == Display.DEFAULT_DISPLAY + || (getForcedWindowFlags() & FLAG_FULLSCREEN) != 0; + if (shouldUseBottomOutset) { + mOutsetBottomPx = ScreenShapeHelper.getWindowOutsetBottomPx( + getContext().getResources().getDisplayMetrics(), a); + } + } + + final Context context = getContext(); + final int targetSdk = context.getApplicationInfo().targetSdkVersion; + final boolean targetPreHoneycomb = targetSdk < android.os.Build.VERSION_CODES.HONEYCOMB; + final boolean targetPreIcs = targetSdk < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH; + final boolean targetPreL = targetSdk < android.os.Build.VERSION_CODES.LOLLIPOP; + final boolean targetHcNeedsOptions = context.getResources().getBoolean( + R.bool.target_honeycomb_needs_options_menu); + final boolean noActionBar = !hasFeature(FEATURE_ACTION_BAR) || hasFeature(FEATURE_NO_TITLE); + + if (targetPreHoneycomb || (targetPreIcs && targetHcNeedsOptions && noActionBar)) { + setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_TRUE); + } else { + setNeedsMenuKey(WindowManager.LayoutParams.NEEDS_MENU_SET_FALSE); + } + + // Non-floating windows on high end devices must put up decor beneath the system bars and + // therefore must know about visibility changes of those. + if (!mIsFloating && ActivityManager.isHighEndGfx()) { + if (!targetPreL && a.getBoolean( + R.styleable.Window_windowDrawsSystemBarBackgrounds, + false)) { + setFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, + FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS & ~getForcedWindowFlags()); + } + } + if (!mForcedStatusBarColor) { + mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000); + } + if (!mForcedNavigationBarColor) { + mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000); + } + if (a.getBoolean(R.styleable.Window_windowLightStatusBar, false)) { + decor.setSystemUiVisibility( + decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + + if (mAlwaysReadCloseOnTouchAttr || getContext().getApplicationInfo().targetSdkVersion + >= android.os.Build.VERSION_CODES.HONEYCOMB) { + if (a.getBoolean( + R.styleable.Window_windowCloseOnTouchOutside, + false)) { + setCloseOnTouchOutsideIfNotSet(true); + } + } + + WindowManager.LayoutParams params = getAttributes(); + + if (!hasSoftInputMode()) { + params.softInputMode = a.getInt( + R.styleable.Window_windowSoftInputMode, + params.softInputMode); + } + + if (a.getBoolean(R.styleable.Window_backgroundDimEnabled, + mIsFloating)) { + /* All dialogs should have the window dimmed */ + if ((getForcedWindowFlags()&WindowManager.LayoutParams.FLAG_DIM_BEHIND) == 0) { + params.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND; + } + if (!haveDimAmount()) { + params.dimAmount = a.getFloat( + android.R.styleable.Window_backgroundDimAmount, 0.5f); + } + } + + if (params.windowAnimations == 0) { + params.windowAnimations = a.getResourceId( + R.styleable.Window_windowAnimationStyle, 0); + } + + // The rest are only done if this window is not embedded; otherwise, + // the values are inherited from our container. + if (getContainer() == null) { + if (mBackgroundDrawable == null) { + if (mBackgroundResource == 0) { + mBackgroundResource = a.getResourceId( + R.styleable.Window_windowBackground, 0); + } + if (mFrameResource == 0) { + mFrameResource = a.getResourceId(R.styleable.Window_windowFrame, 0); + } + mBackgroundFallbackResource = a.getResourceId( + R.styleable.Window_windowBackgroundFallback, 0); + if (false) { + System.out.println("Background: " + + Integer.toHexString(mBackgroundResource) + " Frame: " + + Integer.toHexString(mFrameResource)); + } + } + mElevation = a.getDimension(R.styleable.Window_windowElevation, 0); + mClipToOutline = a.getBoolean(R.styleable.Window_windowClipToOutline, false); + mTextColor = a.getColor(R.styleable.Window_textColor, Color.TRANSPARENT); + } + + // Inflate the window decor. + + int layoutResource; + int features = getLocalFeatures(); + // System.out.println("Features: 0x" + Integer.toHexString(features)); + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + layoutResource = R.layout.screen_swipe_dismiss; + } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) { + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleIconsDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_title_icons; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + // System.out.println("Title Icons!"); + } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0 + && (features & (1 << FEATURE_ACTION_BAR)) == 0) { + // Special case for a window with only a progress bar (and title). + // XXX Need to have a no-title version of embedded windows. + layoutResource = R.layout.screen_progress; + // System.out.println("Progress!"); + } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) { + // Special case for a window with a custom title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogCustomTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else { + layoutResource = R.layout.screen_custom_title; + } + // XXX Remove this once action bar supports these features. + removeFeature(FEATURE_ACTION_BAR); + } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) { + // If no other features and not embedded, only need a title. + // If the window is floating, we need a dialog layout + if (mIsFloating) { + TypedValue res = new TypedValue(); + getContext().getTheme().resolveAttribute( + R.attr.dialogTitleDecorLayout, res, true); + layoutResource = res.resourceId; + } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) { + layoutResource = a.getResourceId( + R.styleable.Window_windowActionBarFullscreenDecorLayout, + R.layout.screen_action_bar); + } else { + layoutResource = R.layout.screen_title; + } + // System.out.println("Title!"); + } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) { + layoutResource = R.layout.screen_simple_overlay_action_mode; + } else { + // Embedded, so no decoration is needed. + layoutResource = R.layout.screen_simple; + // System.out.println("Simple!"); + } + + mDecor.startChanging(); + + View in = mLayoutInflater.inflate(layoutResource, null); + decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + mContentRoot = (ViewGroup) in; + + ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); + if (contentParent == null) { + throw new RuntimeException("Window couldn't find content container view"); + } + + if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) { + ProgressBar progress = getCircularProgressBar(false); + if (progress != null) { + progress.setIndeterminate(true); + } + } + + if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { + registerSwipeCallbacks(); + } + + // Remaining setup -- of background and title -- that only applies + // to top-level windows. + if (getContainer() == null) { + final Drawable background; + if (mBackgroundResource != 0) { + background = getContext().getDrawable(mBackgroundResource); + } else { + background = mBackgroundDrawable; + } + mDecor.setWindowBackground(background); + + final Drawable frame; + if (mFrameResource != 0) { + frame = getContext().getDrawable(mFrameResource); + } else { + frame = null; + } + mDecor.setWindowFrame(frame); + + mDecor.setElevation(mElevation); + mDecor.setClipToOutline(mClipToOutline); + + if (mTitle != null) { + setTitle(mTitle); + } + + if (mTitleColor == 0) { + mTitleColor = mTextColor; + } + setTitleColor(mTitleColor); + } + + mDecor.finishChanging(); + + return contentParent; + } + + /** @hide */ + public void alwaysReadCloseOnTouchAttr() { + mAlwaysReadCloseOnTouchAttr = true; + } + + private void installDecor() { + if (mDecor == null) { + mDecor = generateDecor(); + mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + mDecor.setIsRootNamespace(true); + if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { + mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); + } + } + if (mContentParent == null) { + mContentParent = generateLayout(mDecor); + + // Set up decor part of UI to ignore fitsSystemWindows if appropriate. + mDecor.makeOptionalFitsSystemWindows(); + + final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById( + R.id.decor_content_parent); + + if (decorContentParent != null) { + mDecorContentParent = decorContentParent; + mDecorContentParent.setWindowCallback(getCallback()); + if (mDecorContentParent.getTitle() == null) { + mDecorContentParent.setWindowTitle(mTitle); + } + + final int localFeatures = getLocalFeatures(); + for (int i = 0; i < FEATURE_MAX; i++) { + if ((localFeatures & (1 << i)) != 0) { + mDecorContentParent.initFeature(i); + } + } + + mDecorContentParent.setUiOptions(mUiOptions); + + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 || + (mIconRes != 0 && !mDecorContentParent.hasIcon())) { + mDecorContentParent.setIcon(mIconRes); + } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 && + mIconRes == 0 && !mDecorContentParent.hasIcon()) { + mDecorContentParent.setIcon( + getContext().getPackageManager().getDefaultActivityIcon()); + mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK; + } + if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 || + (mLogoRes != 0 && !mDecorContentParent.hasLogo())) { + mDecorContentParent.setLogo(mLogoRes); + } + + // Invalidate if the panel menu hasn't been created before this. + // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu + // being called in the middle of onCreate or similar. + // A pending invalidation will typically be resolved before the posted message + // would run normally in order to satisfy instance state restoration. + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + if (!isDestroyed() && (st == null || st.menu == null)) { + invalidatePanelMenu(FEATURE_ACTION_BAR); + } + } else { + mTitleView = (TextView)findViewById(R.id.title); + if (mTitleView != null) { + mTitleView.setLayoutDirection(mDecor.getLayoutDirection()); + if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) { + View titleContainer = findViewById( + R.id.title_container); + if (titleContainer != null) { + titleContainer.setVisibility(View.GONE); + } else { + mTitleView.setVisibility(View.GONE); + } + if (mContentParent instanceof FrameLayout) { + ((FrameLayout)mContentParent).setForeground(null); + } + } else { + mTitleView.setText(mTitle); + } + } + } + + if (mDecor.getBackground() == null && mBackgroundFallbackResource != 0) { + mDecor.setBackgroundFallback(mBackgroundFallbackResource); + } + + // Only inflate or create a new TransitionManager if the caller hasn't + // already set a custom one. + if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) { + if (mTransitionManager == null) { + final int transitionRes = getWindowStyle().getResourceId( + R.styleable.Window_windowContentTransitionManager, + 0); + if (transitionRes != 0) { + final TransitionInflater inflater = TransitionInflater.from(getContext()); + mTransitionManager = inflater.inflateTransitionManager(transitionRes, + mContentParent); + } else { + mTransitionManager = new TransitionManager(); + } + } + + mEnterTransition = getTransition(mEnterTransition, null, + R.styleable.Window_windowEnterTransition); + mReturnTransition = getTransition(mReturnTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReturnTransition); + mExitTransition = getTransition(mExitTransition, null, + R.styleable.Window_windowExitTransition); + mReenterTransition = getTransition(mReenterTransition, USE_DEFAULT_TRANSITION, + R.styleable.Window_windowReenterTransition); + mSharedElementEnterTransition = getTransition(mSharedElementEnterTransition, null, + R.styleable.Window_windowSharedElementEnterTransition); + mSharedElementReturnTransition = getTransition(mSharedElementReturnTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReturnTransition); + mSharedElementExitTransition = getTransition(mSharedElementExitTransition, null, + R.styleable.Window_windowSharedElementExitTransition); + mSharedElementReenterTransition = getTransition(mSharedElementReenterTransition, + USE_DEFAULT_TRANSITION, + R.styleable.Window_windowSharedElementReenterTransition); + if (mAllowEnterTransitionOverlap == null) { + mAllowEnterTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowEnterTransitionOverlap, true); + } + if (mAllowReturnTransitionOverlap == null) { + mAllowReturnTransitionOverlap = getWindowStyle().getBoolean( + R.styleable.Window_windowAllowReturnTransitionOverlap, true); + } + if (mBackgroundFadeDurationMillis < 0) { + mBackgroundFadeDurationMillis = getWindowStyle().getInteger( + R.styleable.Window_windowTransitionBackgroundFadeDuration, + DEFAULT_BACKGROUND_FADE_DURATION_MS); + } + if (mSharedElementsUseOverlay == null) { + mSharedElementsUseOverlay = getWindowStyle().getBoolean( + R.styleable.Window_windowSharedElementsUseOverlay, true); + } + } + } + } + + private Transition getTransition(Transition currentValue, Transition defaultValue, int id) { + if (currentValue != defaultValue) { + return currentValue; + } + int transitionId = getWindowStyle().getResourceId(id, -1); + Transition transition = defaultValue; + if (transitionId != -1 && transitionId != R.transition.no_transition) { + TransitionInflater inflater = TransitionInflater.from(getContext()); + transition = inflater.inflateTransition(transitionId); + if (transition instanceof TransitionSet && + ((TransitionSet)transition).getTransitionCount() == 0) { + transition = null; + } + } + return transition; + } + + private Drawable loadImageURI(Uri uri) { + try { + return Drawable.createFromStream( + getContext().getContentResolver().openInputStream(uri), null); + } catch (Exception e) { + Log.w(TAG, "Unable to open content: " + uri); + } + return null; + } + + private DrawableFeatureState getDrawableState(int featureId, boolean required) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + DrawableFeatureState[] ar; + if ((ar = mDrawables) == null || ar.length <= featureId) { + DrawableFeatureState[] nar = new DrawableFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mDrawables = ar = nar; + } + + DrawableFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = new DrawableFeatureState(featureId); + } + return st; + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required) { + return getPanelState(featureId, required, null); + } + + /** + * Gets a panel's state based on its feature ID. + * + * @param featureId The feature ID of the panel. + * @param required Whether the panel is required (if it is required and it + * isn't in our features, this throws an exception). + * @param convertPanelState Optional: If the panel state does not exist, use + * this as the panel state. + * @return The panel state. + */ + private PanelFeatureState getPanelState(int featureId, boolean required, + PanelFeatureState convertPanelState) { + if ((getFeatures() & (1 << featureId)) == 0) { + if (!required) { + return null; + } + throw new RuntimeException("The feature has not been requested"); + } + + PanelFeatureState[] ar; + if ((ar = mPanels) == null || ar.length <= featureId) { + PanelFeatureState[] nar = new PanelFeatureState[featureId + 1]; + if (ar != null) { + System.arraycopy(ar, 0, nar, 0, ar.length); + } + mPanels = ar = nar; + } + + PanelFeatureState st = ar[featureId]; + if (st == null) { + ar[featureId] = st = (convertPanelState != null) + ? convertPanelState + : new PanelFeatureState(featureId); + } + return st; + } + + @Override + public final void setChildDrawable(int featureId, Drawable drawable) { + DrawableFeatureState st = getDrawableState(featureId, true); + st.child = drawable; + updateDrawable(featureId, st, false); + } + + @Override + public final void setChildInt(int featureId, int value) { + updateInt(featureId, value, false); + } + + @Override + public boolean isShortcutKey(int keyCode, KeyEvent event) { + PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false); + return st != null && st.menu != null && st.menu.isShortcutKey(keyCode, event); + } + + private void updateDrawable(int featureId, DrawableFeatureState st, boolean fromResume) { + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + Drawable drawable = null; + if (st != null) { + drawable = st.child; + if (drawable == null) + drawable = st.local; + if (drawable == null) + drawable = st.def; + } + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + if (isActive() || fromResume) { + getContainer().setChildDrawable(featureId, drawable); + } + } + } else if (st != null && (st.cur != drawable || st.curAlpha != st.alpha)) { + // System.out.println("Drawable changed: old=" + st.cur + // + ", new=" + drawable); + st.cur = drawable; + st.curAlpha = st.alpha; + onDrawableChanged(featureId, drawable, st.alpha); + } + } + + private void updateInt(int featureId, int value, boolean fromResume) { + + // Do nothing if the decor is not yet installed... an update will + // need to be forced when we eventually become active. + if (mContentParent == null) { + return; + } + + final int featureMask = 1 << featureId; + + if ((getFeatures() & featureMask) == 0 && !fromResume) { + return; + } + + if ((getLocalFeatures() & featureMask) == 0) { + if (getContainer() != null) { + getContainer().setChildInt(featureId, value); + } + } else { + onIntChanged(featureId, value); + } + } + + private ImageView getLeftIconView() { + if (mLeftIconView != null) { + return mLeftIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mLeftIconView = (ImageView)findViewById(R.id.left_icon)); + } + + @Override + protected void dispatchWindowAttributesChanged(WindowManager.LayoutParams attrs) { + super.dispatchWindowAttributesChanged(attrs); + if (mDecor != null) { + mDecor.updateColorViews(null /* insets */, true /* animate */); + } + } + + private ProgressBar getCircularProgressBar(boolean shouldInstallDecor) { + if (mCircularProgressBar != null) { + return mCircularProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mCircularProgressBar = (ProgressBar) findViewById(R.id.progress_circular); + if (mCircularProgressBar != null) { + mCircularProgressBar.setVisibility(View.INVISIBLE); + } + return mCircularProgressBar; + } + + private ProgressBar getHorizontalProgressBar(boolean shouldInstallDecor) { + if (mHorizontalProgressBar != null) { + return mHorizontalProgressBar; + } + if (mContentParent == null && shouldInstallDecor) { + installDecor(); + } + mHorizontalProgressBar = (ProgressBar) findViewById(R.id.progress_horizontal); + if (mHorizontalProgressBar != null) { + mHorizontalProgressBar.setVisibility(View.INVISIBLE); + } + return mHorizontalProgressBar; + } + + private ImageView getRightIconView() { + if (mRightIconView != null) { + return mRightIconView; + } + if (mContentParent == null) { + installDecor(); + } + return (mRightIconView = (ImageView)findViewById(R.id.right_icon)); + } + + private void registerSwipeCallbacks() { + SwipeDismissLayout swipeDismiss = + (SwipeDismissLayout) findViewById(R.id.content); + swipeDismiss.setOnDismissedListener(new SwipeDismissLayout.OnDismissedListener() { + @Override + public void onDismissed(SwipeDismissLayout layout) { + dispatchOnWindowDismissed(); + } + }); + swipeDismiss.setOnSwipeProgressChangedListener( + new SwipeDismissLayout.OnSwipeProgressChangedListener() { + private static final float ALPHA_DECREASE = 0.5f; + private boolean mIsTranslucent = false; + @Override + public void onSwipeProgressChanged( + SwipeDismissLayout layout, float progress, float translate) { + WindowManager.LayoutParams newParams = getAttributes(); + newParams.x = (int) translate; + newParams.alpha = 1 - (progress * ALPHA_DECREASE); + setAttributes(newParams); + + int flags = 0; + if (newParams.x == 0) { + flags = FLAG_FULLSCREEN; + } else { + flags = FLAG_LAYOUT_NO_LIMITS; + } + setFlags(flags, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS); + } + + @Override + public void onSwipeCancelled(SwipeDismissLayout layout) { + WindowManager.LayoutParams newParams = getAttributes(); + newParams.x = 0; + newParams.alpha = 1; + setAttributes(newParams); + setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN | FLAG_LAYOUT_NO_LIMITS); + } + }); + } + + /** + * Helper method for calling the {@link Callback#onPanelClosed(int, Menu)} + * callback. This method will grab whatever extra state is needed for the + * callback that isn't given in the parameters. If the panel is not open, + * this will not perform the callback. + * + * @param featureId Feature ID of the panel that was closed. Must be given. + * @param panel Panel that was closed. Optional but useful if there is no + * menu given. + * @param menu The menu that was closed. Optional, but give if you have. + */ + private void callOnPanelClosed(int featureId, PanelFeatureState panel, Menu menu) { + final Callback cb = getCallback(); + if (cb == null) + return; + + // Try to get a menu + if (menu == null) { + // Need a panel to grab the menu, so try to get that + if (panel == null) { + if ((featureId >= 0) && (featureId < mPanels.length)) { + panel = mPanels[featureId]; + } + } + + if (panel != null) { + // menu still may be null, which is okay--we tried our best + menu = panel.menu; + } + } + + // If the panel is not open, do not callback + if ((panel != null) && (!panel.isOpen)) + return; + + if (!isDestroyed()) { + cb.onPanelClosed(featureId, menu); + } + } + + /** + * Helper method for adding launch-search to most applications. Opens the + * search window using default settings. + * + * @return true if search window opened + */ + private boolean launchDefaultSearch(KeyEvent event) { + boolean result; + final Callback cb = getCallback(); + if (cb == null || isDestroyed()) { + result = false; + } else { + sendCloseSystemWindows("search"); + int deviceId = event.getDeviceId(); + SearchEvent searchEvent = null; + if (deviceId != 0) { + searchEvent = new SearchEvent(InputDevice.getDevice(deviceId)); + } + try { + result = cb.onSearchRequested(searchEvent); + } catch (AbstractMethodError e) { + Log.e(TAG, "WindowCallback " + cb.getClass().getName() + " does not implement" + + " method onSearchRequested(SearchEvent); fa", e); + result = cb.onSearchRequested(); + } + } + if (!result && (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION) { + // On TVs, if the app doesn't implement search, we want to launch assist. + return ((SearchManager)getContext().getSystemService(Context.SEARCH_SERVICE)) + .launchAssistAction(null, UserHandle.myUserId()); + } + return result; + } + + @Override + public void setVolumeControlStream(int streamType) { + mVolumeControlStreamType = streamType; + } + + @Override + public int getVolumeControlStream() { + return mVolumeControlStreamType; + } + + @Override + public void setMediaController(MediaController controller) { + mMediaController = controller; + } + + @Override + public MediaController getMediaController() { + return mMediaController; + } + + @Override + public void setEnterTransition(Transition enterTransition) { + mEnterTransition = enterTransition; + } + + @Override + public void setReturnTransition(Transition transition) { + mReturnTransition = transition; + } + + @Override + public void setExitTransition(Transition exitTransition) { + mExitTransition = exitTransition; + } + + @Override + public void setReenterTransition(Transition transition) { + mReenterTransition = transition; + } + + @Override + public void setSharedElementEnterTransition(Transition sharedElementEnterTransition) { + mSharedElementEnterTransition = sharedElementEnterTransition; + } + + @Override + public void setSharedElementReturnTransition(Transition transition) { + mSharedElementReturnTransition = transition; + } + + @Override + public void setSharedElementExitTransition(Transition sharedElementExitTransition) { + mSharedElementExitTransition = sharedElementExitTransition; + } + + @Override + public void setSharedElementReenterTransition(Transition transition) { + mSharedElementReenterTransition = transition; + } + + @Override + public Transition getEnterTransition() { + return mEnterTransition; + } + + @Override + public Transition getReturnTransition() { + return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mReturnTransition; + } + + @Override + public Transition getExitTransition() { + return mExitTransition; + } + + @Override + public Transition getReenterTransition() { + return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mReenterTransition; + } + + @Override + public Transition getSharedElementEnterTransition() { + return mSharedElementEnterTransition; + } + + @Override + public Transition getSharedElementReturnTransition() { + return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION + ? getSharedElementEnterTransition() : mSharedElementReturnTransition; + } + + @Override + public Transition getSharedElementExitTransition() { + return mSharedElementExitTransition; + } + + @Override + public Transition getSharedElementReenterTransition() { + return mSharedElementReenterTransition == USE_DEFAULT_TRANSITION + ? getSharedElementExitTransition() : mSharedElementReenterTransition; + } + + @Override + public void setAllowEnterTransitionOverlap(boolean allow) { + mAllowEnterTransitionOverlap = allow; + } + + @Override + public boolean getAllowEnterTransitionOverlap() { + return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap; + } + + @Override + public void setAllowReturnTransitionOverlap(boolean allowExitTransitionOverlap) { + mAllowReturnTransitionOverlap = allowExitTransitionOverlap; + } + + @Override + public boolean getAllowReturnTransitionOverlap() { + return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap; + } + + @Override + public long getTransitionBackgroundFadeDuration() { + return (mBackgroundFadeDurationMillis < 0) ? DEFAULT_BACKGROUND_FADE_DURATION_MS + : mBackgroundFadeDurationMillis; + } + + @Override + public void setTransitionBackgroundFadeDuration(long fadeDurationMillis) { + if (fadeDurationMillis < 0) { + throw new IllegalArgumentException("negative durations are not allowed"); + } + mBackgroundFadeDurationMillis = fadeDurationMillis; + } + + @Override + public void setSharedElementsUseOverlay(boolean sharedElementsUseOverlay) { + mSharedElementsUseOverlay = sharedElementsUseOverlay; + } + + @Override + public boolean getSharedElementsUseOverlay() { + return (mSharedElementsUseOverlay == null) ? true : mSharedElementsUseOverlay; + } + + private static final class DrawableFeatureState { + DrawableFeatureState(int _featureId) { + featureId = _featureId; + } + + final int featureId; + + int resid; + + Uri uri; + + Drawable local; + + Drawable child; + + Drawable def; + + Drawable cur; + + int alpha = 255; + + int curAlpha = 255; + } + + private static final class PanelFeatureState { + + /** Feature ID for this panel. */ + int featureId; + + // Information pulled from the style for this panel. + + int background; + + /** The background when the panel spans the entire available width. */ + int fullBackground; + + int gravity; + + int x; + + int y; + + int windowAnimations; + + /** Dynamic state of the panel. */ + DecorView decorView; + + /** The panel that was returned by onCreatePanelView(). */ + View createdPanelView; + + /** The panel that we are actually showing. */ + View shownPanelView; + + /** Use {@link #setMenu} to set this. */ + MenuBuilder menu; + + IconMenuPresenter iconMenuPresenter; + ListMenuPresenter listMenuPresenter; + + /** true if this menu will show in single-list compact mode */ + boolean isCompact; + + /** Theme resource ID for list elements of the panel menu */ + int listPresenterTheme; + + /** + * Whether the panel has been prepared (see + * {@link PhoneWindow#preparePanel}). + */ + boolean isPrepared; + + /** + * Whether an item's action has been performed. This happens in obvious + * scenarios (user clicks on menu item), but can also happen with + * chording menu+(shortcut key). + */ + boolean isHandled; + + boolean isOpen; + + /** + * True if the menu is in expanded mode, false if the menu is in icon + * mode + */ + boolean isInExpandedMode; + + public boolean qwertyMode; + + boolean refreshDecorView; + + boolean refreshMenuContent; + + boolean wasLastOpen; + + boolean wasLastExpanded; + + /** + * Contains the state of the menu when told to freeze. + */ + Bundle frozenMenuState; + + /** + * Contains the state of associated action views when told to freeze. + * These are saved across invalidations. + */ + Bundle frozenActionViewState; + + PanelFeatureState(int featureId) { + this.featureId = featureId; + + refreshDecorView = false; + } + + public boolean isInListMode() { + return isInExpandedMode || isCompact; + } + + public boolean hasPanelItems() { + if (shownPanelView == null) return false; + if (createdPanelView != null) return true; + + if (isCompact || isInExpandedMode) { + return listMenuPresenter.getAdapter().getCount() > 0; + } else { + return ((ViewGroup) shownPanelView).getChildCount() > 0; + } + } + + /** + * Unregister and free attached MenuPresenters. They will be recreated as needed. + */ + public void clearMenuPresenters() { + if (menu != null) { + menu.removeMenuPresenter(iconMenuPresenter); + menu.removeMenuPresenter(listMenuPresenter); + } + iconMenuPresenter = null; + listMenuPresenter = null; + } + + void setStyle(Context context) { + TypedArray a = context.obtainStyledAttributes(R.styleable.Theme); + background = a.getResourceId( + R.styleable.Theme_panelBackground, 0); + fullBackground = a.getResourceId( + R.styleable.Theme_panelFullBackground, 0); + windowAnimations = a.getResourceId( + R.styleable.Theme_windowAnimationStyle, 0); + isCompact = a.getBoolean( + R.styleable.Theme_panelMenuIsCompact, false); + listPresenterTheme = a.getResourceId( + R.styleable.Theme_panelMenuListTheme, + R.style.Theme_ExpandedMenu); + a.recycle(); + } + + void setMenu(MenuBuilder menu) { + if (menu == this.menu) return; + + if (this.menu != null) { + this.menu.removeMenuPresenter(iconMenuPresenter); + this.menu.removeMenuPresenter(listMenuPresenter); + } + this.menu = menu; + if (menu != null) { + if (iconMenuPresenter != null) menu.addMenuPresenter(iconMenuPresenter); + if (listMenuPresenter != null) menu.addMenuPresenter(listMenuPresenter); + } + } + + MenuView getListMenuView(Context context, MenuPresenter.Callback cb) { + if (menu == null) return null; + + if (!isCompact) { + getIconMenuView(context, cb); // Need this initialized to know where our offset goes + } + + if (listMenuPresenter == null) { + listMenuPresenter = new ListMenuPresenter( + R.layout.list_menu_item_layout, listPresenterTheme); + listMenuPresenter.setCallback(cb); + listMenuPresenter.setId(R.id.list_menu_presenter); + menu.addMenuPresenter(listMenuPresenter); + } + + if (iconMenuPresenter != null) { + listMenuPresenter.setItemIndexOffset( + iconMenuPresenter.getNumActualItemsShown()); + } + MenuView result = listMenuPresenter.getMenuView(decorView); + + return result; + } + + MenuView getIconMenuView(Context context, MenuPresenter.Callback cb) { + if (menu == null) return null; + + if (iconMenuPresenter == null) { + iconMenuPresenter = new IconMenuPresenter(context); + iconMenuPresenter.setCallback(cb); + iconMenuPresenter.setId(R.id.icon_menu_presenter); + menu.addMenuPresenter(iconMenuPresenter); + } + + MenuView result = iconMenuPresenter.getMenuView(decorView); + + return result; + } + + Parcelable onSaveInstanceState() { + SavedState savedState = new SavedState(); + savedState.featureId = featureId; + savedState.isOpen = isOpen; + savedState.isInExpandedMode = isInExpandedMode; + + if (menu != null) { + savedState.menuState = new Bundle(); + menu.savePresenterStates(savedState.menuState); + } + + return savedState; + } + + void onRestoreInstanceState(Parcelable state) { + SavedState savedState = (SavedState) state; + featureId = savedState.featureId; + wasLastOpen = savedState.isOpen; + wasLastExpanded = savedState.isInExpandedMode; + frozenMenuState = savedState.menuState; + + /* + * A LocalActivityManager keeps the same instance of this class around. + * The first time the menu is being shown after restoring, the + * Activity.onCreateOptionsMenu should be called. But, if it is the + * same instance then menu != null and we won't call that method. + * We clear any cached views here. The caller should invalidatePanelMenu. + */ + createdPanelView = null; + shownPanelView = null; + decorView = null; + } + + void applyFrozenState() { + if (menu != null && frozenMenuState != null) { + menu.restorePresenterStates(frozenMenuState); + frozenMenuState = null; + } + } + + private static class SavedState implements Parcelable { + int featureId; + boolean isOpen; + boolean isInExpandedMode; + Bundle menuState; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(featureId); + dest.writeInt(isOpen ? 1 : 0); + dest.writeInt(isInExpandedMode ? 1 : 0); + + if (isOpen) { + dest.writeBundle(menuState); + } + } + + private static SavedState readFromParcel(Parcel source) { + SavedState savedState = new SavedState(); + savedState.featureId = source.readInt(); + savedState.isOpen = source.readInt() == 1; + savedState.isInExpandedMode = source.readInt() == 1; + + if (savedState.isOpen) { + savedState.menuState = source.readBundle(); + } + + return savedState; + } + + public static final Parcelable.Creator<SavedState> CREATOR + = new Parcelable.Creator<SavedState>() { + public SavedState createFromParcel(Parcel in) { + return readFromParcel(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + } + + static class RotationWatcher extends Stub { + private Handler mHandler; + private final Runnable mRotationChanged = new Runnable() { + public void run() { + dispatchRotationChanged(); + } + }; + private final ArrayList<WeakReference<PhoneWindow>> mWindows = + new ArrayList<WeakReference<PhoneWindow>>(); + private boolean mIsWatching; + + @Override + public void onRotationChanged(int rotation) throws RemoteException { + mHandler.post(mRotationChanged); + } + + public void addWindow(PhoneWindow phoneWindow) { + synchronized (mWindows) { + if (!mIsWatching) { + try { + WindowManagerHolder.sWindowManager.watchRotation(this); + mHandler = new Handler(); + mIsWatching = true; + } catch (RemoteException ex) { + Log.e(TAG, "Couldn't start watching for device rotation", ex); + } + } + mWindows.add(new WeakReference<PhoneWindow>(phoneWindow)); + } + } + + public void removeWindow(PhoneWindow phoneWindow) { + synchronized (mWindows) { + int i = 0; + while (i < mWindows.size()) { + final WeakReference<PhoneWindow> ref = mWindows.get(i); + final PhoneWindow win = ref.get(); + if (win == null || win == phoneWindow) { + mWindows.remove(i); + } else { + i++; + } + } + } + } + + void dispatchRotationChanged() { + synchronized (mWindows) { + int i = 0; + while (i < mWindows.size()) { + final WeakReference<PhoneWindow> ref = mWindows.get(i); + final PhoneWindow win = ref.get(); + if (win != null) { + win.onOptionsPanelRotationChanged(); + i++; + } else { + mWindows.remove(i); + } + } + } + } + } + + /** + * Simple implementation of MenuBuilder.Callback that: + * <li> Opens a submenu when selected. + * <li> Calls back to the callback's onMenuItemSelected when an item is + * selected. + */ + private final class DialogMenuCallback implements MenuBuilder.Callback, MenuPresenter.Callback { + private int mFeatureId; + private MenuDialogHelper mSubMenuHelper; + + public DialogMenuCallback(int featureId) { + mFeatureId = featureId; + } + + public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) { + if (menu.getRootMenu() != menu) { + onCloseSubMenu(menu); + } + + if (allMenusAreClosing) { + Callback callback = getCallback(); + if (callback != null && !isDestroyed()) { + callback.onPanelClosed(mFeatureId, menu); + } + + if (menu == mContextMenu) { + dismissContextMenu(); + } + + // Dismiss the submenu, if it is showing + if (mSubMenuHelper != null) { + mSubMenuHelper.dismiss(); + mSubMenuHelper = null; + } + } + } + + public void onCloseSubMenu(MenuBuilder menu) { + Callback callback = getCallback(); + if (callback != null && !isDestroyed()) { + callback.onPanelClosed(mFeatureId, menu.getRootMenu()); + } + } + + public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) { + Callback callback = getCallback(); + return (callback != null && !isDestroyed()) + && callback.onMenuItemSelected(mFeatureId, item); + } + + public void onMenuModeChange(MenuBuilder menu) { + } + + public boolean onOpenSubMenu(MenuBuilder subMenu) { + if (subMenu == null) return false; + + // Set a simple callback for the submenu + subMenu.setCallback(this); + + // The window manager will give us a valid window token + mSubMenuHelper = new MenuDialogHelper(subMenu); + mSubMenuHelper.show(null); + + return true; + } + } + + private static class ColorViewState { + View view = null; + int targetVisibility = View.INVISIBLE; + + final int id; + final int systemUiHideFlag; + final int translucentFlag; + final int verticalGravity; + final String transitionName; + final int hideWindowFlag; + + ColorViewState(int systemUiHideFlag, + int translucentFlag, int verticalGravity, + String transitionName, int id, int hideWindowFlag) { + this.id = id; + this.systemUiHideFlag = systemUiHideFlag; + this.translucentFlag = translucentFlag; + this.verticalGravity = verticalGravity; + this.transitionName = transitionName; + this.hideWindowFlag = hideWindowFlag; + } + } + + void sendCloseSystemWindows() { + sendCloseSystemWindows(getContext(), null); + } + + void sendCloseSystemWindows(String reason) { + sendCloseSystemWindows(getContext(), reason); + } + + public static void sendCloseSystemWindows(Context context, String reason) { + if (ActivityManagerNative.isSystemReady()) { + try { + ActivityManagerNative.getDefault().closeSystemDialogs(reason); + } catch (RemoteException e) { + } + } + } + + @Override + public int getStatusBarColor() { + return mStatusBarColor; + } + + @Override + public void setStatusBarColor(int color) { + mStatusBarColor = color; + mForcedStatusBarColor = true; + if (mDecor != null) { + mDecor.updateColorViews(null, false /* animate */); + } + } + + @Override + public int getNavigationBarColor() { + return mNavigationBarColor; + } + + @Override + public void setNavigationBarColor(int color) { + mNavigationBarColor = color; + mForcedNavigationBarColor = true; + if (mDecor != null) { + mDecor.updateColorViews(null, false /* animate */); + } + } +} diff --git a/core/java/com/android/internal/transition/EpicenterClipReveal.java b/core/java/com/android/internal/transition/EpicenterClipReveal.java deleted file mode 100644 index 1a6736a..0000000 --- a/core/java/com/android/internal/transition/EpicenterClipReveal.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.internal.transition; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.RectEvaluator; -import android.animation.TimeInterpolator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.transition.TransitionValues; -import android.transition.Visibility; -import android.util.AttributeSet; -import android.util.Property; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; -import android.view.animation.PathInterpolator; - -import com.android.internal.R; - -/** - * EpicenterClipReveal captures the {@link View#getClipBounds()} before and - * after the scene change and animates between those and the epicenter bounds - * during a visibility transition. - */ -public class EpicenterClipReveal extends Visibility { - private static final String PROPNAME_CLIP = "android:epicenterReveal:clip"; - private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds"; - - private final TimeInterpolator mInterpolatorX; - private final TimeInterpolator mInterpolatorY; - private final boolean mCenterClipBounds; - - public EpicenterClipReveal() { - mInterpolatorX = null; - mInterpolatorY = null; - mCenterClipBounds = false; - } - - public EpicenterClipReveal(Context context, AttributeSet attrs) { - super(context, attrs); - - final TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.EpicenterClipReveal, 0, 0); - - mCenterClipBounds = a.getBoolean(R.styleable.EpicenterClipReveal_centerClipBounds, false); - - final int interpolatorX = a.getResourceId(R.styleable.EpicenterClipReveal_interpolatorX, 0); - if (interpolatorX != 0) { - mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX); - } else { - mInterpolatorX = TransitionConstants.LINEAR_OUT_SLOW_IN; - } - - final int interpolatorY = a.getResourceId(R.styleable.EpicenterClipReveal_interpolatorY, 0); - if (interpolatorY != 0) { - mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY); - } else { - mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN; - } - - a.recycle(); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - captureValues(transitionValues); - } - - @Override - public void captureEndValues(TransitionValues transitionValues) { - super.captureEndValues(transitionValues); - captureValues(transitionValues); - } - - private void captureValues(TransitionValues values) { - final View view = values.view; - if (view.getVisibility() == View.GONE) { - return; - } - - final Rect clip = view.getClipBounds(); - values.values.put(PROPNAME_CLIP, clip); - - if (clip == null) { - final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); - values.values.put(PROPNAME_BOUNDS, bounds); - } - } - - @Override - public Animator onAppear(ViewGroup sceneRoot, View view, - TransitionValues startValues, TransitionValues endValues) { - if (endValues == null) { - return null; - } - - final Rect end = getBestRect(endValues); - final Rect start = getEpicenterOrCenter(end); - - // Prepare the view. - view.setClipBounds(start); - - return createRectAnimator(view, start, end, endValues, mInterpolatorX, mInterpolatorY); - } - - @Override - public Animator onDisappear(ViewGroup sceneRoot, View view, - TransitionValues startValues, TransitionValues endValues) { - if (startValues == null) { - return null; - } - - final Rect start = getBestRect(startValues); - final Rect end = getEpicenterOrCenter(start); - - // Prepare the view. - view.setClipBounds(start); - - return createRectAnimator(view, start, end, endValues, mInterpolatorX, mInterpolatorY); - } - - private Rect getEpicenterOrCenter(Rect bestRect) { - final Rect epicenter = getEpicenter(); - if (epicenter != null) { - // Translate the clip bounds to be centered within the target bounds. - if (mCenterClipBounds) { - final int offsetX = bestRect.centerX() - epicenter.centerX(); - final int offsetY = bestRect.centerY() - epicenter.centerY(); - epicenter.offset(offsetX, offsetY); - } - return epicenter; - } - - final int centerX = bestRect.centerX(); - final int centerY = bestRect.centerY(); - return new Rect(centerX, centerY, centerX, centerY); - } - - private Rect getBestRect(TransitionValues values) { - final Rect clipRect = (Rect) values.values.get(PROPNAME_CLIP); - if (clipRect == null) { - return (Rect) values.values.get(PROPNAME_BOUNDS); - } - return clipRect; - } - - private static Animator createRectAnimator(final View view, Rect start, Rect end, - TransitionValues endValues, TimeInterpolator interpolatorX, - TimeInterpolator interpolatorY) { - final RectEvaluator evaluator = new RectEvaluator(new Rect()); - final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP); - - final ClipDimenProperty propX = new ClipDimenProperty(ClipDimenProperty.TARGET_X); - final ObjectAnimator animX = ObjectAnimator.ofObject(view, propX, evaluator, start, end); - if (interpolatorX != null) { - animX.setInterpolator(interpolatorX); - } - - final ClipDimenProperty propY = new ClipDimenProperty(ClipDimenProperty.TARGET_Y); - final ObjectAnimator animY = ObjectAnimator.ofObject(view, propY, evaluator, start, end); - if (interpolatorY != null) { - animY.setInterpolator(interpolatorY); - } - - final AnimatorSet animSet = new AnimatorSet(); - animSet.playTogether(animX, animY); - animSet.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - view.setClipBounds(terminalClip); - } - }); - return animSet; - } - - private static class ClipDimenProperty extends Property<View, Rect> { - public static final char TARGET_X = 'x'; - public static final char TARGET_Y = 'y'; - - private final Rect mTempRect = new Rect(); - - private final int mTargetDimension; - - public ClipDimenProperty(char targetDimension) { - super(Rect.class, "clip_bounds_" + targetDimension); - - mTargetDimension = targetDimension; - } - - @Override - public Rect get(View object) { - final Rect tempRect = mTempRect; - if (!object.getClipBounds(tempRect)) { - tempRect.setEmpty(); - } - return tempRect; - } - - @Override - public void set(View object, Rect value) { - final Rect tempRect = mTempRect; - if (object.getClipBounds(tempRect)) { - if (mTargetDimension == TARGET_X) { - tempRect.left = value.left; - tempRect.right = value.right; - } else { - tempRect.top = value.top; - tempRect.bottom = value.bottom; - } - object.setClipBounds(tempRect); - } - } - } -} diff --git a/core/java/com/android/internal/transition/EpicenterTranslate.java b/core/java/com/android/internal/transition/EpicenterTranslate.java deleted file mode 100644 index eac3ff8..0000000 --- a/core/java/com/android/internal/transition/EpicenterTranslate.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2015 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.internal.transition; - -import com.android.internal.R; - -import android.animation.Animator; -import android.animation.AnimatorSet; -import android.animation.ObjectAnimator; -import android.animation.TimeInterpolator; -import android.content.Context; -import android.content.res.TypedArray; -import android.graphics.Rect; -import android.transition.TransitionValues; -import android.transition.Visibility; -import android.util.AttributeSet; -import android.view.View; -import android.view.ViewGroup; -import android.view.animation.AnimationUtils; - -/** - * EpicenterTranslate captures the {@link View#getTranslationX()} and - * {@link View#getTranslationY()} before and after the scene change and - * animates between those and the epicenter's center during a visibility - * transition. - */ -public class EpicenterTranslate extends Visibility { - private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds"; - private static final String PROPNAME_TRANSLATE_X = "android:epicenterReveal:translateX"; - private static final String PROPNAME_TRANSLATE_Y = "android:epicenterReveal:translateY"; - private static final String PROPNAME_TRANSLATE_Z = "android:epicenterReveal:translateZ"; - private static final String PROPNAME_Z = "android:epicenterReveal:z"; - - private final TimeInterpolator mInterpolatorX; - private final TimeInterpolator mInterpolatorY; - private final TimeInterpolator mInterpolatorZ; - - public EpicenterTranslate() { - mInterpolatorX = null; - mInterpolatorY = null; - mInterpolatorZ = null; - } - - public EpicenterTranslate(Context context, AttributeSet attrs) { - super(context, attrs); - - final TypedArray a = context.obtainStyledAttributes(attrs, - R.styleable.EpicenterTranslate, 0, 0); - - final int interpolatorX = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorX, 0); - if (interpolatorX != 0) { - mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX); - } else { - mInterpolatorX = TransitionConstants.FAST_OUT_SLOW_IN; - } - - final int interpolatorY = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorY, 0); - if (interpolatorY != 0) { - mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY); - } else { - mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN; - } - - final int interpolatorZ = a.getResourceId(R.styleable.EpicenterTranslate_interpolatorZ, 0); - if (interpolatorZ != 0) { - mInterpolatorZ = AnimationUtils.loadInterpolator(context, interpolatorZ); - } else { - mInterpolatorZ = TransitionConstants.FAST_OUT_SLOW_IN; - } - - a.recycle(); - } - - @Override - public void captureStartValues(TransitionValues transitionValues) { - super.captureStartValues(transitionValues); - captureValues(transitionValues); - } - - @Override - public void captureEndValues(TransitionValues transitionValues) { - super.captureEndValues(transitionValues); - captureValues(transitionValues); - } - - private void captureValues(TransitionValues values) { - final View view = values.view; - if (view.getVisibility() == View.GONE) { - return; - } - - final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); - values.values.put(PROPNAME_BOUNDS, bounds); - values.values.put(PROPNAME_TRANSLATE_X, view.getTranslationX()); - values.values.put(PROPNAME_TRANSLATE_Y, view.getTranslationY()); - values.values.put(PROPNAME_TRANSLATE_Z, view.getTranslationZ()); - values.values.put(PROPNAME_Z, view.getZ()); - } - - @Override - public Animator onAppear(ViewGroup sceneRoot, View view, - TransitionValues startValues, TransitionValues endValues) { - if (endValues == null) { - return null; - } - - final Rect end = (Rect) endValues.values.get(PROPNAME_BOUNDS); - final Rect start = getEpicenterOrCenter(end); - final float startX = start.centerX() - end.centerX(); - final float startY = start.centerY() - end.centerY(); - final float startZ = 0 - (float) endValues.values.get(PROPNAME_Z); - - // Translate the view to be centered on the epicenter. - view.setTranslationX(startX); - view.setTranslationY(startY); - view.setTranslationZ(startZ); - - final float endX = (float) endValues.values.get(PROPNAME_TRANSLATE_X); - final float endY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y); - final float endZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z); - return createAnimator(view, startX, startY, startZ, endX, endY, endZ, - mInterpolatorX, mInterpolatorY, mInterpolatorZ); - } - - @Override - public Animator onDisappear(ViewGroup sceneRoot, View view, - TransitionValues startValues, TransitionValues endValues) { - if (startValues == null) { - return null; - } - - final Rect start = (Rect) endValues.values.get(PROPNAME_BOUNDS); - final Rect end = getEpicenterOrCenter(start); - final float endX = end.centerX() - start.centerX(); - final float endY = end.centerY() - start.centerY(); - final float endZ = 0 - (float) startValues.values.get(PROPNAME_Z); - - final float startX = (float) endValues.values.get(PROPNAME_TRANSLATE_X); - final float startY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y); - final float startZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z); - return createAnimator(view, startX, startY, startZ, endX, endY, endZ, - mInterpolatorX, mInterpolatorY, mInterpolatorZ); - } - - private Rect getEpicenterOrCenter(Rect bestRect) { - final Rect epicenter = getEpicenter(); - if (epicenter != null) { - return epicenter; - } - - final int centerX = bestRect.centerX(); - final int centerY = bestRect.centerY(); - return new Rect(centerX, centerY, centerX, centerY); - } - - private static Animator createAnimator(final View view, float startX, float startY, - float startZ, float endX, float endY, float endZ, TimeInterpolator interpolatorX, - TimeInterpolator interpolatorY, TimeInterpolator interpolatorZ) { - final ObjectAnimator animX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, startX, endX); - if (interpolatorX != null) { - animX.setInterpolator(interpolatorX); - } - - final ObjectAnimator animY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, startY, endY); - if (interpolatorY != null) { - animY.setInterpolator(interpolatorY); - } - - final ObjectAnimator animZ = ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, startZ, endZ); - if (interpolatorZ != null) { - animZ.setInterpolator(interpolatorZ); - } - - final AnimatorSet animSet = new AnimatorSet(); - animSet.playTogether(animX, animY, animZ); - return animSet; - } -} diff --git a/core/java/com/android/internal/transition/EpicenterTranslateClipReveal.java b/core/java/com/android/internal/transition/EpicenterTranslateClipReveal.java new file mode 100644 index 0000000..2c10297 --- /dev/null +++ b/core/java/com/android/internal/transition/EpicenterTranslateClipReveal.java @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.transition; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.TimeInterpolator; +import android.animation.TypeEvaluator; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.transition.TransitionValues; +import android.transition.Visibility; +import android.util.AttributeSet; +import android.util.Property; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; + +import com.android.internal.R; + +/** + * EpicenterTranslateClipReveal captures the clip bounds and translation values + * before and after the scene change and animates between those and the + * epicenter bounds during a visibility transition. + */ +public class EpicenterTranslateClipReveal extends Visibility { + private static final String PROPNAME_CLIP = "android:epicenterReveal:clip"; + private static final String PROPNAME_BOUNDS = "android:epicenterReveal:bounds"; + private static final String PROPNAME_TRANSLATE_X = "android:epicenterReveal:translateX"; + private static final String PROPNAME_TRANSLATE_Y = "android:epicenterReveal:translateY"; + private static final String PROPNAME_TRANSLATE_Z = "android:epicenterReveal:translateZ"; + private static final String PROPNAME_Z = "android:epicenterReveal:z"; + + private final TimeInterpolator mInterpolatorX; + private final TimeInterpolator mInterpolatorY; + private final TimeInterpolator mInterpolatorZ; + + public EpicenterTranslateClipReveal() { + mInterpolatorX = null; + mInterpolatorY = null; + mInterpolatorZ = null; + } + + public EpicenterTranslateClipReveal(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.EpicenterTranslateClipReveal, 0, 0); + + final int interpolatorX = a.getResourceId( + R.styleable.EpicenterTranslateClipReveal_interpolatorX, 0); + if (interpolatorX != 0) { + mInterpolatorX = AnimationUtils.loadInterpolator(context, interpolatorX); + } else { + mInterpolatorX = TransitionConstants.LINEAR_OUT_SLOW_IN; + } + + final int interpolatorY = a.getResourceId( + R.styleable.EpicenterTranslateClipReveal_interpolatorY, 0); + if (interpolatorY != 0) { + mInterpolatorY = AnimationUtils.loadInterpolator(context, interpolatorY); + } else { + mInterpolatorY = TransitionConstants.FAST_OUT_SLOW_IN; + } + + final int interpolatorZ = a.getResourceId( + R.styleable.EpicenterTranslateClipReveal_interpolatorZ, 0); + if (interpolatorZ != 0) { + mInterpolatorZ = AnimationUtils.loadInterpolator(context, interpolatorZ); + } else { + mInterpolatorZ = TransitionConstants.FAST_OUT_SLOW_IN; + } + + a.recycle(); + } + + @Override + public void captureStartValues(TransitionValues transitionValues) { + super.captureStartValues(transitionValues); + captureValues(transitionValues); + } + + @Override + public void captureEndValues(TransitionValues transitionValues) { + super.captureEndValues(transitionValues); + captureValues(transitionValues); + } + + private void captureValues(TransitionValues values) { + final View view = values.view; + if (view.getVisibility() == View.GONE) { + return; + } + + final Rect bounds = new Rect(0, 0, view.getWidth(), view.getHeight()); + values.values.put(PROPNAME_BOUNDS, bounds); + values.values.put(PROPNAME_TRANSLATE_X, view.getTranslationX()); + values.values.put(PROPNAME_TRANSLATE_Y, view.getTranslationY()); + values.values.put(PROPNAME_TRANSLATE_Z, view.getTranslationZ()); + values.values.put(PROPNAME_Z, view.getZ()); + + final Rect clip = view.getClipBounds(); + values.values.put(PROPNAME_CLIP, clip); + } + + @Override + public Animator onAppear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (endValues == null) { + return null; + } + + final Rect endBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); + final Rect startBounds = getEpicenterOrCenter(endBounds); + final float startX = startBounds.centerX() - endBounds.centerX(); + final float startY = startBounds.centerY() - endBounds.centerY(); + final float startZ = 0 - (float) endValues.values.get(PROPNAME_Z); + + // Translate the view to be centered on the epicenter. + view.setTranslationX(startX); + view.setTranslationY(startY); + view.setTranslationZ(startZ); + + final float endX = (float) endValues.values.get(PROPNAME_TRANSLATE_X); + final float endY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y); + final float endZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z); + + final Rect endClip = getBestRect(endValues); + final Rect startClip = getEpicenterOrCenter(endClip); + + // Prepare the view. + view.setClipBounds(startClip); + + final State startStateX = new State(startClip.left, startClip.right, startX); + final State endStateX = new State(endClip.left, endClip.right, endX); + final State startStateY = new State(startClip.top, startClip.bottom, startY); + final State endStateY = new State(endClip.top, endClip.bottom, endY); + + return createRectAnimator(view, startStateX, startStateY, startZ, endStateX, endStateY, + endZ, endValues, mInterpolatorX, mInterpolatorY, mInterpolatorZ); + } + + @Override + public Animator onDisappear(ViewGroup sceneRoot, View view, + TransitionValues startValues, TransitionValues endValues) { + if (startValues == null) { + return null; + } + + final Rect startBounds = (Rect) endValues.values.get(PROPNAME_BOUNDS); + final Rect endBounds = getEpicenterOrCenter(startBounds); + final float endX = endBounds.centerX() - startBounds.centerX(); + final float endY = endBounds.centerY() - startBounds.centerY(); + final float endZ = 0 - (float) startValues.values.get(PROPNAME_Z); + + final float startX = (float) endValues.values.get(PROPNAME_TRANSLATE_X); + final float startY = (float) endValues.values.get(PROPNAME_TRANSLATE_Y); + final float startZ = (float) endValues.values.get(PROPNAME_TRANSLATE_Z); + + final Rect startClip = getBestRect(startValues); + final Rect endClip = getEpicenterOrCenter(startClip); + + // Prepare the view. + view.setClipBounds(startClip); + + final State startStateX = new State(startClip.left, startClip.right, startX); + final State endStateX = new State(endClip.left, endClip.right, endX); + final State startStateY = new State(startClip.top, startClip.bottom, startY); + final State endStateY = new State(endClip.top, endClip.bottom, endY); + + return createRectAnimator(view, startStateX, startStateY, startZ, endStateX, endStateY, + endZ, endValues, mInterpolatorX, mInterpolatorY, mInterpolatorZ); + } + + private Rect getEpicenterOrCenter(Rect bestRect) { + final Rect epicenter = getEpicenter(); + if (epicenter != null) { + return epicenter; + } + + final int centerX = bestRect.centerX(); + final int centerY = bestRect.centerY(); + return new Rect(centerX, centerY, centerX, centerY); + } + + private Rect getBestRect(TransitionValues values) { + final Rect clipRect = (Rect) values.values.get(PROPNAME_CLIP); + if (clipRect == null) { + return (Rect) values.values.get(PROPNAME_BOUNDS); + } + return clipRect; + } + + private static Animator createRectAnimator(final View view, State startX, State startY, + float startZ, State endX, State endY, float endZ, TransitionValues endValues, + TimeInterpolator interpolatorX, TimeInterpolator interpolatorY, + TimeInterpolator interpolatorZ) { + final StateEvaluator evaluator = new StateEvaluator(); + + final ObjectAnimator animZ = ObjectAnimator.ofFloat(view, View.TRANSLATION_Z, startZ, endZ); + if (interpolatorZ != null) { + animZ.setInterpolator(interpolatorZ); + } + + final StateProperty propX = new StateProperty(StateProperty.TARGET_X); + final ObjectAnimator animX = ObjectAnimator.ofObject(view, propX, evaluator, startX, endX); + if (interpolatorX != null) { + animX.setInterpolator(interpolatorX); + } + + final StateProperty propY = new StateProperty(StateProperty.TARGET_Y); + final ObjectAnimator animY = ObjectAnimator.ofObject(view, propY, evaluator, startY, endY); + if (interpolatorY != null) { + animY.setInterpolator(interpolatorY); + } + + final Rect terminalClip = (Rect) endValues.values.get(PROPNAME_CLIP); + final AnimatorListenerAdapter animatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + view.setClipBounds(terminalClip); + } + }; + + final AnimatorSet animSet = new AnimatorSet(); + animSet.playTogether(animX, animY, animZ); + animSet.addListener(animatorListener); + return animSet; + } + + private static class State { + int lower; + int upper; + float trans; + + public State() {} + + public State(int lower, int upper, float trans) { + this.lower = lower; + this.upper = upper; + this.trans = trans; + } + } + + private static class StateEvaluator implements TypeEvaluator<State> { + private final State mTemp = new State(); + + @Override + public State evaluate(float fraction, State startValue, State endValue) { + mTemp.upper = startValue.upper + (int) ((endValue.upper - startValue.upper) * fraction); + mTemp.lower = startValue.lower + (int) ((endValue.lower - startValue.lower) * fraction); + mTemp.trans = startValue.trans + (int) ((endValue.trans - startValue.trans) * fraction); + return mTemp; + } + } + + private static class StateProperty extends Property<View, State> { + public static final char TARGET_X = 'x'; + public static final char TARGET_Y = 'y'; + + private final Rect mTempRect = new Rect(); + private final State mTempState = new State(); + + private final int mTargetDimension; + + public StateProperty(char targetDimension) { + super(State.class, "state_" + targetDimension); + + mTargetDimension = targetDimension; + } + + @Override + public State get(View object) { + final Rect tempRect = mTempRect; + if (!object.getClipBounds(tempRect)) { + tempRect.setEmpty(); + } + final State tempState = mTempState; + if (mTargetDimension == TARGET_X) { + tempState.trans = object.getTranslationX(); + tempState.lower = tempRect.left + (int) tempState.trans; + tempState.upper = tempRect.right + (int) tempState.trans; + } else { + tempState.trans = object.getTranslationY(); + tempState.lower = tempRect.top + (int) tempState.trans; + tempState.upper = tempRect.bottom + (int) tempState.trans; + } + return tempState; + } + + @Override + public void set(View object, State value) { + final Rect tempRect = mTempRect; + if (object.getClipBounds(tempRect)) { + if (mTargetDimension == TARGET_X) { + tempRect.left = value.lower - (int) value.trans; + tempRect.right = value.upper - (int) value.trans; + } else { + tempRect.top = value.lower - (int) value.trans; + tempRect.bottom = value.upper - (int) value.trans; + } + object.setClipBounds(tempRect); + } + + if (mTargetDimension == TARGET_X) { + object.setTranslationX(value.trans); + } else { + object.setTranslationY(value.trans); + } + } + } +} diff --git a/core/java/com/android/internal/util/AsyncChannel.java b/core/java/com/android/internal/util/AsyncChannel.java index 34f62ba..bd0e6ce 100644 --- a/core/java/com/android/internal/util/AsyncChannel.java +++ b/core/java/com/android/internal/util/AsyncChannel.java @@ -462,10 +462,8 @@ public class AsyncChannel { } catch(Exception e) { } // Tell source we're disconnected. - if (mSrcHandler != null) { - replyDisconnected(STATUS_SUCCESSFUL); - mSrcHandler = null; - } + replyDisconnected(STATUS_SUCCESSFUL); + mSrcHandler = null; // Unlink only when bindService isn't used if (mConnection == null && mDstMessenger != null && mDeathMonitor!= null) { mDstMessenger.getBinder().unlinkToDeath(mDeathMonitor, 0); @@ -871,6 +869,8 @@ public class AsyncChannel { * @param status to be stored in msg.arg1 */ private void replyDisconnected(int status) { + // Can't reply if already disconnected. Avoid NullPointerException. + if (mSrcHandler == null) return; Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_DISCONNECTED); msg.arg1 = status; msg.obj = this; diff --git a/core/java/com/android/internal/util/CallbackRegistry.java b/core/java/com/android/internal/util/CallbackRegistry.java new file mode 100644 index 0000000..0f228d4 --- /dev/null +++ b/core/java/com/android/internal/util/CallbackRegistry.java @@ -0,0 +1,395 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tracks callbacks for the event. This class supports reentrant modification + * of the callbacks during notification without adversely disrupting notifications. + * A common pattern for callbacks is to receive a notification and then remove + * themselves. This class handles this behavior with constant memory under + * most circumstances. + * + * <p>A subclass of {@link CallbackRegistry.NotifierCallback} must be passed to + * the constructor to define how notifications should be called. That implementation + * does the actual notification on the listener.</p> + * + * <p>This class supports only callbacks with at most two parameters. + * Typically, these are the notification originator and a parameter, but these may + * be used as required. If more than two parameters are required or primitive types + * must be used, <code>A</code> should be some kind of containing structure that + * the subclass may reuse between notifications.</p> + * + * @param <C> The callback type. + * @param <T> The notification sender type. Typically this is the containing class. + * @param <A> Opaque argument used to pass additional data beyond an int. + */ +public class CallbackRegistry<C, T, A> implements Cloneable { + private static final String TAG = "CallbackRegistry"; + + /** An ordered collection of listeners waiting to be notified. */ + private List<C> mCallbacks = new ArrayList<C>(); + + /** + * A bit flag for the first 64 listeners that are removed during notification. + * The lowest significant bit corresponds to the 0th index into mCallbacks. + * For a small number of callbacks, no additional array of objects needs to + * be allocated. + */ + private long mFirst64Removed = 0x0; + + /** + * Bit flags for the remaining callbacks that are removed during notification. + * When there are more than 64 callbacks and one is marked for removal, a dynamic + * array of bits are allocated for the callbacks. + */ + private long[] mRemainderRemoved; + + /** + * The reentrancy level of the notification. When we notify a callback, it may cause + * further notifications. The reentrancy level must be tracked to let us clean up + * the callback state when all notifications have been processed. + */ + private int mNotificationLevel; + + /** The notification mechanism for notifying an event. */ + private final NotifierCallback<C, T, A> mNotifier; + + /** + * Creates an EventRegistry that notifies the event with notifier. + * @param notifier The class to use to notify events. + */ + public CallbackRegistry(NotifierCallback<C, T, A> notifier) { + mNotifier = notifier; + } + + /** + * Notify all callbacks. + * + * @param sender The originator. This is an opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg2 An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + */ + public synchronized void notifyCallbacks(T sender, int arg, A arg2) { + mNotificationLevel++; + notifyRecurseLocked(sender, arg, arg2); + mNotificationLevel--; + if (mNotificationLevel == 0) { + if (mRemainderRemoved != null) { + for (int i = mRemainderRemoved.length - 1; i >= 0; i--) { + final long removedBits = mRemainderRemoved[i]; + if (removedBits != 0) { + removeRemovedCallbacks((i + 1) * Long.SIZE, removedBits); + mRemainderRemoved[i] = 0; + } + } + } + if (mFirst64Removed != 0) { + removeRemovedCallbacks(0, mFirst64Removed); + mFirst64Removed = 0; + } + } + } + + /** + * Notify up to the first Long.SIZE callbacks that don't have a bit set in <code>removed</code>. + * + * @param sender The originator. This is an opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg2 An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + */ + private void notifyFirst64Locked(T sender, int arg, A arg2) { + final int maxNotified = Math.min(Long.SIZE, mCallbacks.size()); + notifyCallbacksLocked(sender, arg, arg2, 0, maxNotified, mFirst64Removed); + } + + /** + * Notify all callbacks using a recursive algorithm to avoid allocating on the heap. + * This part captures the callbacks beyond Long.SIZE that have no bits allocated for + * removal before it recurses into {@link #notifyRemainderLocked(Object, int, A, int)}. + * <p> + * Recursion is used to avoid allocating temporary state on the heap. Each stack has one + * long (64 callbacks) worth of information of which has been removed. + * + * @param sender The originator. This is an opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg2 An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + */ + private void notifyRecurseLocked(T sender, int arg, A arg2) { + final int callbackCount = mCallbacks.size(); + final int remainderIndex = mRemainderRemoved == null ? -1 : mRemainderRemoved.length - 1; + + // Now we've got all callbacks that have no mRemainderRemoved value, so notify the + // others. + notifyRemainderLocked(sender, arg, arg2, remainderIndex); + + // notifyRemainderLocked notifies all at maxIndex, so we'd normally start at maxIndex + 1 + // However, we must also keep track of those in mFirst64Removed, so we add 2 instead: + final int startCallbackIndex = (remainderIndex + 2) * Long.SIZE; + + // The remaining have no bit set + notifyCallbacksLocked(sender, arg, arg2, startCallbackIndex, callbackCount, 0); + } + + /** + * Notify callbacks that have mRemainderRemoved bits set for remainderIndex. If + * remainderIndex is -1, the first 64 will be notified instead. + * + * @param sender The originator. This is an opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg2 An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param remainderIndex The index into mRemainderRemoved that should be notified. + */ + private void notifyRemainderLocked(T sender, int arg, A arg2, int remainderIndex) { + if (remainderIndex < 0) { + notifyFirst64Locked(sender, arg, arg2); + } else { + final long bits = mRemainderRemoved[remainderIndex]; + final int startIndex = (remainderIndex + 1) * Long.SIZE; + final int endIndex = Math.min(mCallbacks.size(), startIndex + Long.SIZE); + notifyRemainderLocked(sender, arg, arg2, remainderIndex - 1); + notifyCallbacksLocked(sender, arg, arg2, startIndex, endIndex, bits); + } + } + + /** + * Notify callbacks from startIndex to endIndex, using bits as the bit status + * for whether they have been removed or not. bits should be from mRemainderRemoved or + * mFirst64Removed. bits set to 0 indicates that all callbacks from startIndex to + * endIndex should be notified. + * + * @param sender The originator. This is an opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param arg2 An opaque parameter passed to + * {@link CallbackRegistry.NotifierCallback#onNotifyCallback(Object, Object, int, A)} + * @param startIndex The index into the mCallbacks to start notifying. + * @param endIndex One past the last index into mCallbacks to notify. + * @param bits A bit field indicating which callbacks have been removed and shouldn't + * be notified. + */ + private void notifyCallbacksLocked(T sender, int arg, A arg2, final int startIndex, + final int endIndex, final long bits) { + long bitMask = 1; + for (int i = startIndex; i < endIndex; i++) { + if ((bits & bitMask) == 0) { + mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2); + } + bitMask <<= 1; + } + } + + /** + * Add a callback to be notified. If the callback is already in the list, another won't + * be added. This does not affect current notifications. + * @param callback The callback to add. + */ + public synchronized void add(C callback) { + int index = mCallbacks.lastIndexOf(callback); + if (index < 0 || isRemovedLocked(index)) { + mCallbacks.add(callback); + } + } + + /** + * Returns true if the callback at index has been marked for removal. + * + * @param index The index into mCallbacks to check. + * @return true if the callback at index has been marked for removal. + */ + private boolean isRemovedLocked(int index) { + if (index < Long.SIZE) { + // It is in the first 64 callbacks, just check the bit. + final long bitMask = 1L << index; + return (mFirst64Removed & bitMask) != 0; + } else if (mRemainderRemoved == null) { + // It is after the first 64 callbacks, but nothing else was marked for removal. + return false; + } else { + final int maskIndex = (index / Long.SIZE) - 1; + if (maskIndex >= mRemainderRemoved.length) { + // There are some items in mRemainderRemoved, but nothing at the given index. + return false; + } else { + // There is something marked for removal, so we have to check the bit. + final long bits = mRemainderRemoved[maskIndex]; + final long bitMask = 1L << (index % Long.SIZE); + return (bits & bitMask) != 0; + } + } + } + + /** + * Removes callbacks from startIndex to startIndex + Long.SIZE, based + * on the bits set in removed. + * @param startIndex The index into the mCallbacks to start removing callbacks. + * @param removed The bits indicating removal, where each bit is set for one callback + * to be removed. + */ + private void removeRemovedCallbacks(int startIndex, long removed) { + // The naive approach should be fine. There may be a better bit-twiddling approach. + final int endIndex = startIndex + Long.SIZE; + + long bitMask = 1L << (Long.SIZE - 1); + for (int i = endIndex - 1; i >= startIndex; i--) { + if ((removed & bitMask) != 0) { + mCallbacks.remove(i); + } + bitMask >>>= 1; + } + } + + /** + * Remove a callback. This callback won't be notified after this call completes. + * @param callback The callback to remove. + */ + public synchronized void remove(C callback) { + if (mNotificationLevel == 0) { + mCallbacks.remove(callback); + } else { + int index = mCallbacks.lastIndexOf(callback); + if (index >= 0) { + setRemovalBitLocked(index); + } + } + } + + private void setRemovalBitLocked(int index) { + if (index < Long.SIZE) { + // It is in the first 64 callbacks, just check the bit. + final long bitMask = 1L << index; + mFirst64Removed |= bitMask; + } else { + final int remainderIndex = (index / Long.SIZE) - 1; + if (mRemainderRemoved == null) { + mRemainderRemoved = new long[mCallbacks.size() / Long.SIZE]; + } else if (mRemainderRemoved.length < remainderIndex) { + // need to make it bigger + long[] newRemainders = new long[mCallbacks.size() / Long.SIZE]; + System.arraycopy(mRemainderRemoved, 0, newRemainders, 0, mRemainderRemoved.length); + mRemainderRemoved = newRemainders; + } + final long bitMask = 1L << (index % Long.SIZE); + mRemainderRemoved[remainderIndex] |= bitMask; + } + } + + /** + * Makes a copy of the registered callbacks and returns it. + * + * @return a copy of the registered callbacks. + */ + public synchronized ArrayList<C> copyListeners() { + ArrayList<C> callbacks = new ArrayList<C>(mCallbacks.size()); + int numListeners = mCallbacks.size(); + for (int i = 0; i < numListeners; i++) { + if (!isRemovedLocked(i)) { + callbacks.add(mCallbacks.get(i)); + } + } + return callbacks; + } + + /** + * Returns true if there are no registered callbacks or false otherwise. + * + * @return true if there are no registered callbacks or false otherwise. + */ + public synchronized boolean isEmpty() { + if (mCallbacks.isEmpty()) { + return true; + } else if (mNotificationLevel == 0) { + return false; + } else { + int numListeners = mCallbacks.size(); + for (int i = 0; i < numListeners; i++) { + if (!isRemovedLocked(i)) { + return false; + } + } + return true; + } + } + + /** + * Removes all callbacks from the list. + */ + public synchronized void clear() { + if (mNotificationLevel == 0) { + mCallbacks.clear(); + } else if (!mCallbacks.isEmpty()) { + for (int i = mCallbacks.size() - 1; i >= 0; i--) { + setRemovalBitLocked(i); + } + } + } + + public synchronized CallbackRegistry<C, T, A> clone() { + CallbackRegistry<C, T, A> clone = null; + try { + clone = (CallbackRegistry<C, T, A>) super.clone(); + clone.mFirst64Removed = 0; + clone.mRemainderRemoved = null; + clone.mNotificationLevel = 0; + clone.mCallbacks = new ArrayList<C>(); + final int numListeners = mCallbacks.size(); + for (int i = 0; i < numListeners; i++) { + if (!isRemovedLocked(i)) { + clone.mCallbacks.add(mCallbacks.get(i)); + } + } + } catch (CloneNotSupportedException e) { + e.printStackTrace(); + } + return clone; + } + + /** + * Class used to notify events from CallbackRegistry. + * + * @param <C> The callback type. + * @param <T> The notification sender type. Typically this is the containing class. + * @param <A> An opaque argument to pass to the notifier + */ + public abstract static class NotifierCallback<C, T, A> { + /** + * Used to notify the callback. + * + * @param callback The callback to notify. + * @param sender The opaque sender object. + * @param arg The opaque notification parameter. + * @param arg2 An opaque argument passed in + * {@link CallbackRegistry#notifyCallbacks} + * @see CallbackRegistry#CallbackRegistry(CallbackRegistry.NotifierCallback) + */ + public abstract void onNotifyCallback(C callback, T sender, int arg, A arg2); + } +} diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index 6f104dd..60c5e42 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -59,7 +59,8 @@ interface IInputMethodManager { int controlFlags, int softInputMode, int windowFlags, in EditorInfo attribute, IInputContext inputContext); - void showInputMethodPickerFromClient(in IInputMethodClient client); + void showInputMethodPickerFromClient(in IInputMethodClient client, + int auxiliarySubtypeMode); void showInputMethodAndSubtypeEnablerFromClient(in IInputMethodClient client, String topId); void setInputMethod(in IBinder token, String id); void setInputMethodAndSubtype(in IBinder token, String id, in InputMethodSubtype subtype); diff --git a/core/java/com/android/internal/widget/ButtonBarLayout.java b/core/java/com/android/internal/widget/ButtonBarLayout.java index 64e6c69..f58ab03 100644 --- a/core/java/com/android/internal/widget/ButtonBarLayout.java +++ b/core/java/com/android/internal/widget/ButtonBarLayout.java @@ -17,6 +17,7 @@ package com.android.internal.widget; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; @@ -29,37 +30,39 @@ import com.android.internal.R; * orientation when it can't fit its child views horizontally. */ public class ButtonBarLayout extends LinearLayout { - /** Spacer used in horizontal orientation. */ - private final View mSpacer; - /** Whether the current configuration allows stacking. */ - private final boolean mAllowStacked; + private final boolean mAllowStacking; /** Whether the layout is currently stacked. */ private boolean mStacked; + private int mLastWidthSize = -1; + public ButtonBarLayout(Context context, AttributeSet attrs) { super(context, attrs); - mAllowStacked = context.getResources().getBoolean(R.bool.allow_stacked_button_bar); - mSpacer = findViewById(R.id.spacer); + final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ButtonBarLayout); + mAllowStacking = ta.getBoolean(R.styleable.ButtonBarLayout_allowStacking, false); + ta.recycle(); + + mStacked = getOrientation() == VERTICAL; } @Override - protected void onSizeChanged(int w, int h, int oldw, int oldh) { - super.onSizeChanged(w, h, oldw, oldh); + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (mAllowStacking) { + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + if (widthSize > mLastWidthSize && mStacked) { + // We're being measured wider this time, try un-stacking. + setStacked(false); + } - // Maybe we can fit the content now? - if (w > oldw && mStacked) { - setStacked(false); + mLastWidthSize = widthSize; } - } - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); - if (mAllowStacked && getOrientation() == LinearLayout.HORIZONTAL) { + if (mAllowStacking && !mStacked) { final int measuredWidth = getMeasuredWidthAndState(); final int measuredWidthState = measuredWidth & MEASURED_STATE_MASK; if (measuredWidthState == MEASURED_STATE_TOO_SMALL) { @@ -75,8 +78,9 @@ public class ButtonBarLayout extends LinearLayout { setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL); setGravity(stacked ? Gravity.RIGHT : Gravity.BOTTOM); - if (mSpacer != null) { - mSpacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); + final View spacer = findViewById(R.id.spacer); + if (spacer != null) { + spacer.setVisibility(stacked ? View.GONE : View.INVISIBLE); } // Reverse the child order. This is specific to the Material button diff --git a/core/java/com/android/internal/widget/ViewPager.java b/core/java/com/android/internal/widget/ViewPager.java index 441e640..e76302b 100644 --- a/core/java/com/android/internal/widget/ViewPager.java +++ b/core/java/com/android/internal/widget/ViewPager.java @@ -47,6 +47,8 @@ import android.view.animation.Interpolator; import android.widget.EdgeEffect; import android.widget.Scroller; +import com.android.internal.R; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -2720,10 +2722,12 @@ public class ViewPager extends ViewGroup { if (canScrollHorizontally(1)) { info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD); + info.addAction(AccessibilityAction.ACTION_SCROLL_RIGHT); } if (canScrollHorizontally(-1)) { info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD); + info.addAction(AccessibilityAction.ACTION_SCROLL_LEFT); } } @@ -2735,12 +2739,14 @@ public class ViewPager extends ViewGroup { switch (action) { case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: + case R.id.accessibilityActionScrollRight: if (canScrollHorizontally(1)) { setCurrentItem(mCurItem + 1); return true; } return false; case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: + case R.id.accessibilityActionScrollLeft: if (canScrollHorizontally(-1)) { setCurrentItem(mCurItem - 1); return true; |