From ec6bc41e1835804533fd21b3ef8047ecd893bcfd Mon Sep 17 00:00:00 2001 From: Adam Powell Date: Wed, 27 Jan 2016 09:09:35 -0800 Subject: Pinning components in ChooserActivity Move ChooserActivity to SystemUI. This is a safer place for it to live and still be able to persist data to storage. Add a context menu to long press for chooser targets allowing users to 'pin' a target component from an app. This causes it to sort to the front of the list so that a user's favorite apps are always available from share UIs, etc. Similarly, all ChooserTargets from a pinned component receive an impossibly large boost for sorting so that they will always appear first. Bug 26791843 Change-Id: Ib4e603d9d4263403e98ce619287452ddab593044 --- .../com/android/internal/app/ChooserActivity.java | 1422 ------------------- .../internal/app/IntentForwarderActivity.java | 2 +- .../com/android/internal/app/ResolverActivity.java | 103 +- .../android/internal/app/ResolverComparator.java | 30 +- core/res/AndroidManifest.xml | 13 - packages/SystemUI/AndroidManifest.xml | 19 + packages/SystemUI/res/values/arrays.xml | 12 + packages/SystemUI/res/values/strings.xml | 5 + .../android/systemui/chooser/ChooserActivity.java | 1467 ++++++++++++++++++++ .../ResolverTargetActionsDialogFragment.java | 98 ++ 10 files changed, 1692 insertions(+), 1479 deletions(-) delete mode 100644 core/java/com/android/internal/app/ChooserActivity.java create mode 100644 packages/SystemUI/src/com/android/systemui/chooser/ChooserActivity.java create mode 100644 packages/SystemUI/src/com/android/systemui/chooser/ResolverTargetActionsDialogFragment.java diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java deleted file mode 100644 index 27333915b2b6..000000000000 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ /dev/null @@ -1,1422 +0,0 @@ -/* - * 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.app; - -import android.animation.ObjectAnimator; -import android.annotation.NonNull; -import android.app.Activity; -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.LabeledIntent; -import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.database.DataSetObserver; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.Icon; -import android.os.Bundle; -import android.os.Handler; -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.os.UserManager; -import android.provider.DocumentsContract; -import android.service.chooser.ChooserTarget; -import android.service.chooser.ChooserTargetService; -import android.service.chooser.IChooserTargetResult; -import android.service.chooser.IChooserTargetService; -import android.text.TextUtils; -import android.util.FloatProperty; -import android.util.Log; -import android.util.Slog; -import android.view.LayoutInflater; -import android.view.View; -import android.view.View.MeasureSpec; -import android.view.View.OnClickListener; -import android.view.View.OnLongClickListener; -import android.view.ViewGroup; -import android.view.ViewGroup.LayoutParams; -import android.view.animation.AnimationUtils; -import android.view.animation.Interpolator; -import android.widget.AbsListView; -import android.widget.BaseAdapter; -import android.widget.ListView; -import com.android.internal.R; -import com.android.internal.logging.MetricsLogger; -import com.android.internal.logging.MetricsProto.MetricsEvent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -public class ChooserActivity extends ResolverActivity { - private static final String TAG = "ChooserActivity"; - - private static final boolean DEBUG = false; - - 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 Intent mReferrerFillInIntent; - - private ChooserListAdapter mChooserListAdapter; - private ChooserRowAdapter mChooserRowAdapter; - - private final List mServiceConnections = new ArrayList<>(); - - private static final int CHOOSER_TARGET_SERVICE_RESULT = 1; - private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT = 2; - - private final Handler mChooserHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - switch (msg.what) { - case CHOOSER_TARGET_SERVICE_RESULT: - if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_SERVICE_RESULT"); - if (isDestroyed()) break; - final ServiceResultInfo sri = (ServiceResultInfo) msg.obj; - if (!mServiceConnections.contains(sri.connection)) { - Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection - + " returned after being removed from active connections." - + " Have you considered returning results faster?"); - break; - } - if (sri.resultTargets != null) { - mChooserListAdapter.addServiceResults(sri.originalTarget, - sri.resultTargets); - } - unbindService(sri.connection); - sri.connection.destroy(); - mServiceConnections.remove(sri.connection); - if (mServiceConnections.isEmpty()) { - mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); - sendVoiceChoicesIfNeeded(); - } - break; - - case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT: - if (DEBUG) { - Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services"); - } - unbindRemainingServices(); - sendVoiceChoicesIfNeeded(); - break; - - default: - super.handleMessage(msg); - } - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - Intent intent = getIntent(); - Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT); - if (!(targetParcelable instanceof Intent)) { - Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable); - finish(); - super.onCreate(null); - return; - } - Intent target = (Intent) targetParcelable; - 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; - if (title == null) { - defaultTitleRes = com.android.internal.R.string.chooseActivity; - } - Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS); - Intent[] initialIntents = null; - if (pa != null) { - initialIntents = new Intent[pa.length]; - for (int i=0; i 0 - && resolve.activityInfo != null && DocumentsContract.PACKAGE_DOCUMENTS_UI - .equals(resolve.activityInfo.packageName)) { - return true; - } - - return false; - } - - private void modifyTargetIntent(Intent in) { - final String action = in.getAction(); - if (Intent.ACTION_SEND.equals(action) || - Intent.ACTION_SEND_MULTIPLE.equals(action)) { - in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | - Intent.FLAG_ACTIVITY_MULTIPLE_TASK); - } - } - - @Override - protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { - if (mRefinementIntentSender != null) { - final Intent fillIn = new Intent(); - final List 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); - } - - @Override - void startSelected(int which, boolean always, boolean filtered) { - super.startSelected(which, always, filtered); - - if (mChooserListAdapter != null) { - // Log the index of which type of target the user picked. - // Lower values mean the ranking was better. - int cat = 0; - int value = which; - switch (mChooserListAdapter.getPositionTargetType(which)) { - case ChooserListAdapter.TARGET_CALLER: - cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; - break; - case ChooserListAdapter.TARGET_SERVICE: - cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET; - value -= mChooserListAdapter.getCallerTargetCount(); - break; - case ChooserListAdapter.TARGET_STANDARD: - cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET; - value -= mChooserListAdapter.getCallerTargetCount() - + mChooserListAdapter.getServiceTargetCount(); - break; - } - - if (cat != 0) { - MetricsLogger.action(this, cat, value); - } - } - } - - void queryTargetServices(ChooserListAdapter adapter) { - final PackageManager pm = getPackageManager(); - int targetsToQuery = 0; - for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) { - final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i); - if (adapter.getScore(dri) == 0) { - // A score of 0 means the app hasn't been used in some time; - // don't query it as it's not likely to be relevant. - continue; - } - final ActivityInfo ai = dri.getResolveInfo().activityInfo; - final Bundle md = ai.metaData; - final String serviceName = md != null ? convertServiceName(ai.packageName, - md.getString(ChooserTargetService.META_DATA_NAME)) : null; - if (serviceName != null) { - final ComponentName serviceComponent = new ComponentName( - ai.packageName, serviceName); - final Intent serviceIntent = new Intent(ChooserTargetService.SERVICE_INTERFACE) - .setComponent(serviceComponent); - - if (DEBUG) { - Log.d(TAG, "queryTargets found target with service " + serviceComponent); - } - - try { - final String perm = pm.getServiceInfo(serviceComponent, 0).permission; - if (!ChooserTargetService.BIND_PERMISSION.equals(perm)) { - Log.w(TAG, "ChooserTargetService " + serviceComponent + " does not require" - + " permission " + ChooserTargetService.BIND_PERMISSION - + " - this service will not be queried for ChooserTargets." - + " add android:permission=\"" - + ChooserTargetService.BIND_PERMISSION + "\"" - + " to the tag for " + serviceComponent - + " in the manifest."); - continue; - } - } catch (NameNotFoundException e) { - Log.e(TAG, "Could not look up service " + serviceComponent, e); - continue; - } - - final ChooserTargetServiceConnection conn = - new ChooserTargetServiceConnection(this, dri); - if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND, - UserHandle.CURRENT)) { - if (DEBUG) { - Log.d(TAG, "Binding service connection for target " + dri - + " intent " + serviceIntent); - } - mServiceConnections.add(conn); - targetsToQuery++; - } - } - if (targetsToQuery >= QUERY_TARGET_SERVICE_LIMIT) { - if (DEBUG) Log.d(TAG, "queryTargets hit query target limit " - + QUERY_TARGET_SERVICE_LIMIT); - break; - } - } - - if (!mServiceConnections.isEmpty()) { - if (DEBUG) Log.d(TAG, "queryTargets setting watchdog timer for " - + WATCHDOG_TIMEOUT_MILLIS + "ms"); - mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT, - WATCHDOG_TIMEOUT_MILLIS); - } else { - sendVoiceChoicesIfNeeded(); - } - } - - private String convertServiceName(String packageName, String serviceName) { - if (TextUtils.isEmpty(serviceName)) { - return null; - } - - final String fullName; - if (serviceName.startsWith(".")) { - // Relative to the app package. Prepend the app package name. - fullName = packageName + serviceName; - } else if (serviceName.indexOf('.') >= 0) { - // Fully qualified package name. - fullName = serviceName; - } else { - fullName = null; - } - return fullName; - } - - void unbindRemainingServices() { - if (DEBUG) { - Log.d(TAG, "unbindRemainingServices, " + mServiceConnections.size() + " left"); - } - for (int i = 0, N = mServiceConnections.size(); i < N; i++) { - final ChooserTargetServiceConnection conn = mServiceConnections.get(i); - if (DEBUG) Log.d(TAG, "unbinding " + conn); - unbindService(conn); - conn.destroy(); - } - mServiceConnections.clear(); - mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); - } - - void onSetupVoiceInteraction() { - // Do nothing. We'll send the voice stuff ourselves. - } - - 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 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; - } - - void filterServiceTargets(String packageName, List targets) { - if (targets == null) { - return; - } - - final PackageManager pm = getPackageManager(); - for (int i = targets.size() - 1; i >= 0; i--) { - final ChooserTarget target = targets.get(i); - final ComponentName targetName = target.getComponentName(); - if (packageName != null && packageName.equals(targetName.getPackageName())) { - // Anything from the original target's package is fine. - continue; - } - - boolean remove; - try { - final ActivityInfo ai = pm.getActivityInfo(targetName, 0); - remove = !ai.exported || ai.permission != null; - } catch (NameNotFoundException e) { - Log.e(TAG, "Target " + target + " returned by " + packageName - + " component not found"); - remove = true; - } - - if (remove) { - targets.remove(i); - } - } - } - - @Override - ResolveListAdapter createAdapter(Context context, List payloadIntents, - Intent[] initialIntents, List rList, int launchedFromUid, - boolean filterLastUsed) { - final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents, - initialIntents, rList, launchedFromUid, filterLastUsed); - if (DEBUG) Log.d(TAG, "Adapter created; querying services"); - queryTargetServices(adapter); - return adapter; - } - - final class ChooserTargetInfo implements TargetInfo { - private final DisplayResolveInfo mSourceInfo; - private final ResolveInfo mBackupResolveInfo; - private final ChooserTarget mChooserTarget; - private Drawable mBadgeIcon = null; - private CharSequence mBadgeContentDescription; - private Drawable mDisplayIcon; - private final Intent mFillInIntent; - private final int mFillInFlags; - private final float mModifiedScore; - - public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, - float modifiedScore) { - mSourceInfo = sourceInfo; - mChooserTarget = chooserTarget; - mModifiedScore = modifiedScore; - if (sourceInfo != null) { - final ResolveInfo ri = sourceInfo.getResolveInfo(); - if (ri != null) { - final ActivityInfo ai = ri.activityInfo; - if (ai != null && ai.applicationInfo != null) { - final PackageManager pm = getPackageManager(); - mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); - mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); - } - } - } - final Icon icon = chooserTarget.getIcon(); - // TODO do this in the background - mDisplayIcon = icon != null ? icon.loadDrawable(ChooserActivity.this) : null; - - if (sourceInfo != null) { - mBackupResolveInfo = null; - } 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; - mBadgeIcon = other.mBadgeIcon; - mBadgeContentDescription = other.mBadgeContentDescription; - mDisplayIcon = other.mDisplayIcon; - mFillInIntent = fillInIntent; - mFillInFlags = flags; - mModifiedScore = other.mModifiedScore; - } - - public float getModifiedScore() { - return mModifiedScore; - } - - @Override - public Intent getResolvedIntent() { - if (mSourceInfo != null) { - return mSourceInfo.getResolvedIntent(); - } - return getTargetIntent(); - } - - @Override - public ComponentName getResolvedComponentName() { - if (mSourceInfo != null) { - return mSourceInfo.getResolvedComponentName(); - } else if (mBackupResolveInfo != null) { - return new ComponentName(mBackupResolveInfo.activityInfo.packageName, - mBackupResolveInfo.activityInfo.name); - } - return null; - } - - private Intent getBaseIntentToSend() { - Intent result = mSourceInfo != null - ? mSourceInfo.getResolvedIntent() : getTargetIntent(); - if (result == null) { - Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); - } else { - result = new Intent(result); - if (mFillInIntent != null) { - result.fillIn(mFillInIntent, mFillInFlags); - } - result.fillIn(mReferrerFillInIntent, 0); - } - return result; - } - - @Override - public boolean start(Activity activity, Bundle options) { - throw new RuntimeException("ChooserTargets should be started as caller."); - } - - @Override - public boolean startAsCaller(Activity activity, Bundle options, int userId) { - final Intent intent = getBaseIntentToSend(); - if (intent == null) { - return false; - } - intent.setComponent(mChooserTarget.getComponentName()); - intent.putExtras(mChooserTarget.getIntentExtras()); - activity.startActivityAsCaller(intent, options, true, userId); - return true; - } - - @Override - public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { - throw new RuntimeException("ChooserTargets should be started as caller."); - } - - @Override - public ResolveInfo getResolveInfo() { - return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; - } - - @Override - public CharSequence getDisplayLabel() { - return mChooserTarget.getTitle(); - } - - @Override - public CharSequence getExtendedInfo() { - // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. - return null; - } - - @Override - public Drawable getDisplayIcon() { - return mDisplayIcon; - } - - @Override - public Drawable getBadgeIcon() { - return mBadgeIcon; - } - - @Override - public CharSequence getBadgeContentDescription() { - return mBadgeContentDescription; - } - - @Override - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { - return new ChooserTargetInfo(this, fillInIntent, flags); - } - - @Override - public List getAllSourceIntents() { - final List 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 { - public static final int TARGET_BAD = -1; - public static final int TARGET_CALLER = 0; - public static final int TARGET_SERVICE = 1; - public static final int TARGET_STANDARD = 2; - - private static final int MAX_SERVICE_TARGETS = 8; - - private final List mServiceTargets = new ArrayList<>(); - private final List mCallerTargets = new ArrayList<>(); - - private float mLateFee = 1.f; - - private final BaseChooserTargetComparator mBaseTargetComparator - = new BaseChooserTargetComparator(); - - public ChooserListAdapter(Context context, List payloadIntents, - Intent[] initialIntents, List rList, int launchedFromUid, - boolean filterLastUsed) { - // Don't send the initial intents through the shared ResolverActivity path, - // we want to separate them into a different section. - super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed); - - if (initialIntents != null) { - final PackageManager pm = getPackageManager(); - for (int i = 0; i < initialIntents.length; i++) { - final Intent ii = initialIntents[i]; - if (ii == null) { - continue; - } - final ActivityInfo ai = ii.resolveActivityInfo(pm, 0); - if (ai == null) { - Log.w(TAG, "No activity found for " + ii); - continue; - } - ResolveInfo ri = new ResolveInfo(); - ri.activityInfo = ai; - UserManager userManager = - (UserManager) getSystemService(Context.USER_SERVICE); - if (ii instanceof LabeledIntent) { - LabeledIntent li = (LabeledIntent)ii; - ri.resolvePackageName = li.getSourcePackage(); - ri.labelRes = li.getLabelResource(); - ri.nonLocalizedLabel = li.getNonLocalizedLabel(); - ri.icon = li.getIconResource(); - ri.iconResourceId = ri.icon; - } - if (userManager.isManagedProfile()) { - ri.noResourceId = true; - ri.icon = 0; - } - mCallerTargets.add(new DisplayResolveInfo(ii, ri, - ri.loadLabel(pm), null, ii)); - } - } - } - - @Override - public boolean showsExtendedInfo(TargetInfo info) { - // We have badges so we don't need this text shown. - return false; - } - - @Override - public View onCreateView(ViewGroup parent) { - return mInflater.inflate( - com.android.internal.R.layout.resolve_grid_item, parent, false); - } - - @Override - public void onListRebuilt() { - if (mServiceTargets != null) { - pruneServiceTargets(); - } - } - - @Override - public boolean shouldGetResolvedFilter() { - return true; - } - - @Override - public int getCount() { - return super.getCount() + getServiceTargetCount() + getCallerTargetCount(); - } - - @Override - public int getUnfilteredCount() { - return super.getUnfilteredCount() + getServiceTargetCount() + getCallerTargetCount(); - } - - public int getCallerTargetCount() { - return mCallerTargets.size(); - } - - public int getServiceTargetCount() { - return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); - } - - public int getStandardTargetCount() { - return super.getCount(); - } - - public int getPositionTargetType(int position) { - int offset = 0; - - final int callerTargetCount = getCallerTargetCount(); - if (position < callerTargetCount) { - return TARGET_CALLER; - } - offset += callerTargetCount; - - final int serviceTargetCount = getServiceTargetCount(); - if (position - offset < serviceTargetCount) { - return TARGET_SERVICE; - } - offset += serviceTargetCount; - - final int standardTargetCount = super.getCount(); - if (position - offset < standardTargetCount) { - return TARGET_STANDARD; - } - - return TARGET_BAD; - } - - @Override - public TargetInfo getItem(int position) { - return targetInfoForPosition(position, true); - } - - @Override - public TargetInfo targetInfoForPosition(int position, boolean filtered) { - int offset = 0; - - final int callerTargetCount = getCallerTargetCount(); - if (position < callerTargetCount) { - return mCallerTargets.get(position); - } - offset += callerTargetCount; - - final int serviceTargetCount = getServiceTargetCount(); - if (position - offset < serviceTargetCount) { - return mServiceTargets.get(position - offset); - } - offset += serviceTargetCount; - - return filtered ? super.getItem(position - offset) - : getDisplayInfoAt(position - offset); - } - - public void addServiceResults(DisplayResolveInfo origTarget, List targets) { - if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() - + " targets"); - final float parentScore = getScore(origTarget); - Collections.sort(targets, mBaseTargetComparator); - float lastScore = 0; - for (int i = 0, N = targets.size(); i < N; i++) { - final ChooserTarget target = targets.get(i); - float targetScore = target.getScore(); - targetScore *= parentScore; - targetScore *= mLateFee; - if (i > 0 && targetScore >= lastScore) { - // Apply a decay so that the top app can't crowd out everything else. - // This incents ChooserTargetServices to define what's truly better. - targetScore = lastScore * 0.95f; - } - insertServiceTarget(new ChooserTargetInfo(origTarget, target, targetScore)); - - if (DEBUG) { - Log.d(TAG, " => " + target.toString() + " score=" + targetScore - + " base=" + target.getScore() - + " lastScore=" + lastScore - + " parentScore=" + parentScore - + " lateFee=" + mLateFee); - } - - lastScore = targetScore; - } - - mLateFee *= 0.95f; - - notifyDataSetChanged(); - } - - private void insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { - final float newScore = chooserTargetInfo.getModifiedScore(); - for (int i = 0, N = mServiceTargets.size(); i < N; i++) { - final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); - if (newScore > serviceTarget.getModifiedScore()) { - mServiceTargets.add(i, chooserTargetInfo); - return; - } - } - mServiceTargets.add(chooserTargetInfo); - } - - private void pruneServiceTargets() { - if (DEBUG) Log.d(TAG, "pruneServiceTargets"); - for (int i = mServiceTargets.size() - 1; i >= 0; i--) { - final ChooserTargetInfo cti = mServiceTargets.get(i); - if (!hasResolvedTarget(cti.getResolveInfo())) { - if (DEBUG) Log.d(TAG, " => " + i + " " + cti); - mServiceTargets.remove(i); - } - } - } - } - - static class BaseChooserTargetComparator implements Comparator { - @Override - public int compare(ChooserTarget lhs, ChooserTarget rhs) { - // Descending order - return (int) Math.signum(rhs.getScore() - lhs.getScore()); - } - } - - static class RowScale { - private static final int DURATION = 400; - - float mScale; - ChooserRowAdapter mAdapter; - private final ObjectAnimator mAnimator; - - public static final FloatProperty PROPERTY = - new FloatProperty("scale") { - @Override - public void setValue(RowScale object, float value) { - object.mScale = value; - object.mAdapter.notifyDataSetChanged(); - } - - @Override - public Float get(RowScale object) { - return object.mScale; - } - }; - - public RowScale(@NonNull ChooserRowAdapter adapter, float from, float to) { - mAdapter = adapter; - mScale = from; - if (from == to) { - mAnimator = null; - return; - } - - mAnimator = ObjectAnimator.ofFloat(this, PROPERTY, from, to).setDuration(DURATION); - } - - public RowScale setInterpolator(Interpolator interpolator) { - if (mAnimator != null) { - mAnimator.setInterpolator(interpolator); - } - return this; - } - - public float get() { - return mScale; - } - - public void startAnimation() { - if (mAnimator != null) { - mAnimator.start(); - } - } - - public void cancelAnimation() { - if (mAnimator != null) { - mAnimator.cancel(); - } - } - } - - class ChooserRowAdapter extends BaseAdapter { - private ChooserListAdapter mChooserListAdapter; - private final LayoutInflater mLayoutInflater; - private final int mColumnCount = 4; - private RowScale[] mServiceTargetScale; - private final Interpolator mInterpolator; - - public ChooserRowAdapter(ChooserListAdapter wrappedAdapter) { - mChooserListAdapter = wrappedAdapter; - mLayoutInflater = LayoutInflater.from(ChooserActivity.this); - - mInterpolator = AnimationUtils.loadInterpolator(ChooserActivity.this, - android.R.interpolator.decelerate_quint); - - wrappedAdapter.registerDataSetObserver(new DataSetObserver() { - @Override - public void onChanged() { - super.onChanged(); - final int rcount = getServiceTargetRowCount(); - if (mServiceTargetScale == null - || mServiceTargetScale.length != rcount) { - RowScale[] old = mServiceTargetScale; - int oldRCount = old != null ? old.length : 0; - mServiceTargetScale = new RowScale[rcount]; - if (old != null && rcount > 0) { - System.arraycopy(old, 0, mServiceTargetScale, 0, - Math.min(old.length, rcount)); - } - - for (int i = rcount; i < oldRCount; i++) { - old[i].cancelAnimation(); - } - - for (int i = oldRCount; i < rcount; i++) { - final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f) - .setInterpolator(mInterpolator); - mServiceTargetScale[i] = rs; - } - - // Start the animations in a separate loop. - // The process of starting animations will result in - // binding views to set up initial values, and we must - // have ALL of the new RowScale objects created above before - // we get started. - for (int i = oldRCount; i < rcount; i++) { - mServiceTargetScale[i].startAnimation(); - } - } - - notifyDataSetChanged(); - } - - @Override - public void onInvalidated() { - super.onInvalidated(); - notifyDataSetInvalidated(); - if (mServiceTargetScale != null) { - for (RowScale rs : mServiceTargetScale) { - rs.cancelAnimation(); - } - } - } - }); - } - - private float getRowScale(int rowPosition) { - final int start = getCallerTargetRowCount(); - final int end = start + getServiceTargetRowCount(); - if (rowPosition >= start && rowPosition < end) { - return mServiceTargetScale[rowPosition - start].get(); - } - return 1.f; - } - - @Override - public int getCount() { - return (int) ( - getCallerTargetRowCount() - + getServiceTargetRowCount() - + Math.ceil((float) mChooserListAdapter.getStandardTargetCount() / mColumnCount) - ); - } - - public int getCallerTargetRowCount() { - return (int) Math.ceil( - (float) mChooserListAdapter.getCallerTargetCount() / mColumnCount); - } - - public int getServiceTargetRowCount() { - return (int) Math.ceil( - (float) mChooserListAdapter.getServiceTargetCount() / mColumnCount); - } - - @Override - public Object getItem(int position) { - // We have nothing useful to return here. - return position; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - final RowViewHolder holder; - if (convertView == null) { - holder = createViewHolder(parent); - } else { - holder = (RowViewHolder) convertView.getTag(); - } - bindViewHolder(position, holder); - - return holder.row; - } - - RowViewHolder createViewHolder(ViewGroup parent) { - final ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, - parent, false); - final RowViewHolder holder = new RowViewHolder(row, mColumnCount); - final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - - for (int i = 0; i < mColumnCount; i++) { - final View v = mChooserListAdapter.createView(row); - final int column = i; - v.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - startSelected(holder.itemIndices[column], false, true); - } - }); - v.setOnLongClickListener(new OnLongClickListener() { - @Override - public boolean onLongClick(View v) { - showAppDetails( - mChooserListAdapter.resolveInfoForPosition( - holder.itemIndices[column], true)); - return true; - } - }); - row.addView(v); - holder.cells[i] = v; - - // Force height to be a given so we don't have visual disruption during scaling. - LayoutParams lp = v.getLayoutParams(); - v.measure(spec, spec); - if (lp == null) { - lp = new LayoutParams(LayoutParams.MATCH_PARENT, v.getMeasuredHeight()); - row.setLayoutParams(lp); - } else { - lp.height = v.getMeasuredHeight(); - } - } - - // Pre-measure so we can scale later. - holder.measure(); - LayoutParams lp = row.getLayoutParams(); - if (lp == null) { - lp = new LayoutParams(LayoutParams.MATCH_PARENT, holder.measuredRowHeight); - row.setLayoutParams(lp); - } else { - lp.height = holder.measuredRowHeight; - } - row.setTag(holder); - return holder; - } - - void bindViewHolder(int rowPosition, RowViewHolder holder) { - final int start = getFirstRowPosition(rowPosition); - final int startType = mChooserListAdapter.getPositionTargetType(start); - - int end = start + mColumnCount - 1; - while (mChooserListAdapter.getPositionTargetType(end) != startType && end >= start) { - end--; - } - - if (startType == ChooserListAdapter.TARGET_SERVICE) { - holder.row.setBackgroundColor( - getColor(R.color.chooser_service_row_background_color)); - } else { - holder.row.setBackgroundColor(Color.TRANSPARENT); - } - - final int oldHeight = holder.row.getLayoutParams().height; - holder.row.getLayoutParams().height = Math.max(1, - (int) (holder.measuredRowHeight * getRowScale(rowPosition))); - if (holder.row.getLayoutParams().height != oldHeight) { - holder.row.requestLayout(); - } - - for (int i = 0; i < mColumnCount; i++) { - final View v = holder.cells[i]; - if (start + i <= end) { - v.setVisibility(View.VISIBLE); - holder.itemIndices[i] = start + i; - mChooserListAdapter.bindView(holder.itemIndices[i], v); - } else { - v.setVisibility(View.GONE); - } - } - } - - int getFirstRowPosition(int row) { - final int callerCount = mChooserListAdapter.getCallerTargetCount(); - final int callerRows = (int) Math.ceil((float) callerCount / mColumnCount); - - if (row < callerRows) { - return row * mColumnCount; - } - - final int serviceCount = mChooserListAdapter.getServiceTargetCount(); - final int serviceRows = (int) Math.ceil((float) serviceCount / mColumnCount); - - if (row < callerRows + serviceRows) { - return callerCount + (row - callerRows) * mColumnCount; - } - - return callerCount + serviceCount - + (row - callerRows - serviceRows) * mColumnCount; - } - } - - static class RowViewHolder { - final View[] cells; - final ViewGroup row; - int measuredRowHeight; - int[] itemIndices; - - public RowViewHolder(ViewGroup row, int cellCount) { - this.row = row; - this.cells = new View[cellCount]; - this.itemIndices = new int[cellCount]; - } - - public void measure() { - final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); - row.measure(spec, spec); - measuredRowHeight = row.getMeasuredHeight(); - } - } - - static class ChooserTargetServiceConnection implements ServiceConnection { - private final DisplayResolveInfo mOriginalTarget; - private ComponentName mConnectedComponent; - private ChooserActivity mChooserActivity; - private final Object mLock = new Object(); - - private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() { - @Override - public void sendResult(List targets) throws RemoteException { - synchronized (mLock) { - if (mChooserActivity == null) { - Log.e(TAG, "destroyed ChooserTargetServiceConnection received result from " - + mConnectedComponent + "; ignoring..."); - return; - } - mChooserActivity.filterServiceTargets( - mOriginalTarget.getResolveInfo().activityInfo.packageName, targets); - final Message msg = Message.obtain(); - msg.what = CHOOSER_TARGET_SERVICE_RESULT; - msg.obj = new ServiceResultInfo(mOriginalTarget, targets, - ChooserTargetServiceConnection.this); - mChooserActivity.mChooserHandler.sendMessage(msg); - } - } - }; - - public ChooserTargetServiceConnection(ChooserActivity chooserActivity, - DisplayResolveInfo dri) { - mChooserActivity = chooserActivity; - mOriginalTarget = dri; - } - - @Override - public void onServiceConnected(ComponentName name, IBinder service) { - if (DEBUG) Log.d(TAG, "onServiceConnected: " + name); - synchronized (mLock) { - if (mChooserActivity == null) { - Log.e(TAG, "destroyed ChooserTargetServiceConnection got onServiceConnected"); - return; - } - - final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service); - try { - icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(), - mOriginalTarget.getResolveInfo().filter, mChooserTargetResult); - } catch (RemoteException e) { - Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e); - mChooserActivity.unbindService(this); - destroy(); - mChooserActivity.mServiceConnections.remove(this); - } - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name); - synchronized (mLock) { - if (mChooserActivity == null) { - Log.e(TAG, - "destroyed ChooserTargetServiceConnection got onServiceDisconnected"); - return; - } - - mChooserActivity.unbindService(this); - destroy(); - mChooserActivity.mServiceConnections.remove(this); - if (mChooserActivity.mServiceConnections.isEmpty()) { - mChooserActivity.mChooserHandler.removeMessages( - CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); - mChooserActivity.sendVoiceChoicesIfNeeded(); - } - mConnectedComponent = null; - } - } - - public void destroy() { - synchronized (mLock) { - mChooserActivity = null; - } - } - - @Override - public String toString() { - return "ChooserTargetServiceConnection{service=" - + mConnectedComponent + ", activity=" - + mOriginalTarget.getResolveInfo().activityInfo.toString() + "}"; - } - } - - static class ServiceResultInfo { - public final DisplayResolveInfo originalTarget; - public final List resultTargets; - public final ChooserTargetServiceConnection connection; - - public ServiceResultInfo(DisplayResolveInfo ot, List rt, - ChooserTargetServiceConnection c) { - originalTarget = ot; - resultTargets = rt; - 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; - } - } - - class OffsetDataSetObserver extends DataSetObserver { - private final AbsListView mListView; - private int mCachedViewType = -1; - private View mCachedView; - - public OffsetDataSetObserver(AbsListView listView) { - mListView = listView; - } - - @Override - public void onChanged() { - if (mResolverDrawerLayout == null) { - return; - } - - final int chooserTargetRows = mChooserRowAdapter.getServiceTargetRowCount(); - int offset = 0; - for (int i = 0; i < chooserTargetRows; i++) { - final int pos = mChooserRowAdapter.getCallerTargetRowCount() + i; - final int vt = mChooserRowAdapter.getItemViewType(pos); - if (vt != mCachedViewType) { - mCachedView = null; - } - final View v = mChooserRowAdapter.getView(pos, mCachedView, mListView); - int height = ((RowViewHolder) (v.getTag())).measuredRowHeight; - - offset += (int) (height * mChooserRowAdapter.getRowScale(pos)); - - if (vt >= 0) { - mCachedViewType = vt; - mCachedView = v; - } else { - mCachedViewType = -1; - } - } - - mResolverDrawerLayout.setCollapsibleHeightReserved(offset); - } - } -} diff --git a/core/java/com/android/internal/app/IntentForwarderActivity.java b/core/java/com/android/internal/app/IntentForwarderActivity.java index dbec7405fe49..12449871bad9 100644 --- a/core/java/com/android/internal/app/IntentForwarderActivity.java +++ b/core/java/com/android/internal/app/IntentForwarderActivity.java @@ -100,7 +100,7 @@ public class IntentForwarderActivity extends Activity { final boolean shouldShowDisclosure = ri == null || ri.activityInfo == null || !"android".equals(ri.activityInfo.packageName) || !(ResolverActivity.class.getName().equals(ri.activityInfo.name) - || ChooserActivity.class.getName().equals(ri.activityInfo.name)); + || "com.android.systemui.chooser.ChooserActivity".equals(ri.activityInfo.name)); try { startActivityAsCaller(newIntent, null, false, targetUserId); diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java index aa38de737484..4e48c1b4c166 100644 --- a/core/java/com/android/internal/app/ResolverActivity.java +++ b/core/java/com/android/internal/app/ResolverActivity.java @@ -22,6 +22,7 @@ import android.app.ActivityThread; import android.app.VoiceInteractor.PickOptionRequest; import android.app.VoiceInteractor.PickOptionRequest.Option; import android.app.VoiceInteractor.Prompt; +import android.content.pm.ComponentInfo; import android.os.AsyncTask; import android.provider.Settings; import android.text.TextUtils; @@ -336,12 +337,12 @@ public class ResolverActivity extends Activity { /** * Perform any initialization needed for voice interaction. */ - void onSetupVoiceInteraction() { + public void onSetupVoiceInteraction() { // Do it right now. Subclasses may delay this and send it later. sendVoiceChoicesIfNeeded(); } - void sendVoiceChoicesIfNeeded() { + public void sendVoiceChoicesIfNeeded() { if (!isVoiceInteraction()) { // Clearly not needed. return; @@ -382,7 +383,7 @@ public class ResolverActivity extends Activity { return null; } - int getLayoutResource() { + public int getLayoutResource() { return R.layout.resolver_list; } @@ -591,7 +592,7 @@ public class ResolverActivity extends Activity { mAlwaysUseOption); } - void startSelected(int which, boolean always, boolean filtered) { + public void startSelected(int which, boolean always, boolean filtered) { if (isFinishing()) { return; } @@ -761,7 +762,7 @@ public class ResolverActivity extends Activity { return true; } - void safelyStartActivity(TargetInfo cti) { + public void safelyStartActivity(TargetInfo cti) { // If needed, show that intent is forwarded // from managed profile to owner or other way around. if (mProfileSwitchMessageId != -1) { @@ -791,26 +792,26 @@ public class ResolverActivity extends Activity { } } - void onActivityStarted(TargetInfo cti) { + public void onActivityStarted(TargetInfo cti) { // Do nothing } - boolean shouldGetActivityMetadata() { + public boolean shouldGetActivityMetadata() { return false; } - boolean shouldAutoLaunchSingleChoice(TargetInfo target) { + public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { return true; } - void showAppDetails(ResolveInfo ri) { + public void showTargetDetails(ResolveInfo ri) { Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) .setData(Uri.fromParts("package", ri.activityInfo.packageName, null)) .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); startActivity(in); } - ResolveListAdapter createAdapter(Context context, List payloadIntents, + public ResolveListAdapter createAdapter(Context context, List payloadIntents, Intent[] initialIntents, List rList, int launchedFromUid, boolean filterLastUsed) { return new ResolveListAdapter(context, payloadIntents, initialIntents, rList, @@ -820,7 +821,7 @@ public class ResolverActivity extends Activity { /** * Returns true if the activity is finishing and creation should halt */ - boolean configureContentView(List payloadIntents, Intent[] initialIntents, + public boolean configureContentView(List payloadIntents, Intent[] initialIntents, List rList, boolean alwaysUseOption) { // The last argument of createAdapter is whether to do special handling // of the last used choice to highlight it in the list. We need to always @@ -867,7 +868,7 @@ public class ResolverActivity extends Activity { return false; } - void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter, + public void onPrepareAdapterView(AbsListView adapterView, ResolveListAdapter adapter, boolean alwaysUseOption) { final boolean useHeader = adapter.hasFilteredItem(); final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null; @@ -898,7 +899,7 @@ public class ResolverActivity extends Activity { && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName); } - final class DisplayResolveInfo implements TargetInfo { + public final class DisplayResolveInfo implements TargetInfo { private final ResolveInfo mResolveInfo; private final CharSequence mDisplayLabel; private Drawable mDisplayIcon; @@ -906,8 +907,9 @@ public class ResolverActivity extends Activity { private final CharSequence mExtendedInfo; private final Intent mResolvedIntent; private final List mSourceIntents = new ArrayList<>(); + private boolean mPinned; - DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, + public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel, CharSequence pInfo, Intent pOrigIntent) { mSourceIntents.add(originalIntent); mResolveInfo = pri; @@ -932,6 +934,7 @@ public class ResolverActivity extends Activity { mExtendedInfo = other.mExtendedInfo; mResolvedIntent = new Intent(other.mResolvedIntent); mResolvedIntent.fillIn(fillInIntent, flags); + mPinned = other.mPinned; } public ResolveInfo getResolveInfo() { @@ -1026,6 +1029,15 @@ public class ResolverActivity extends Activity { activity.startActivityAsUser(mResolvedIntent, options, user); return false; } + + @Override + public boolean isPinned() { + return mPinned; + } + + public void setPinned(boolean pinned) { + mPinned = pinned; + } } /** @@ -1039,7 +1051,7 @@ public class ResolverActivity extends Activity { * * @return the resolved intent for this target */ - public Intent getResolvedIntent(); + Intent getResolvedIntent(); /** * Get the resolved component name that represents this target. Note that this may not @@ -1048,7 +1060,7 @@ public class ResolverActivity extends Activity { * * @return the resolved ComponentName for this target */ - public ComponentName getResolvedComponentName(); + ComponentName getResolvedComponentName(); /** * Start the activity referenced by this target. @@ -1057,7 +1069,7 @@ public class ResolverActivity extends Activity { * @param options ActivityOptions bundle * @return true if the start completed successfully */ - public boolean start(Activity activity, Bundle options); + boolean start(Activity activity, Bundle options); /** * Start the activity referenced by this target as if the ResolverActivity's caller @@ -1068,7 +1080,7 @@ public class ResolverActivity extends Activity { * @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller * @return true if the start completed successfully */ - public boolean startAsCaller(Activity activity, Bundle options, int userId); + boolean startAsCaller(Activity activity, Bundle options, int userId); /** * Start the activity referenced by this target as a given user. @@ -1078,7 +1090,7 @@ public class ResolverActivity extends Activity { * @param user handle for the user to start the activity as * @return true if the start completed successfully */ - public boolean startAsUser(Activity activity, Bundle options, UserHandle user); + boolean startAsUser(Activity activity, Bundle options, UserHandle user); /** * Return the ResolveInfo about how and why this target matched the original query @@ -1086,14 +1098,14 @@ public class ResolverActivity extends Activity { * * @return ResolveInfo representing this target's match */ - public ResolveInfo getResolveInfo(); + ResolveInfo getResolveInfo(); /** * Return the human-readable text label for this target. * * @return user-visible target label */ - public CharSequence getDisplayLabel(); + CharSequence getDisplayLabel(); /** * Return any extended info for this target. This may be used to disambiguate @@ -1101,35 +1113,40 @@ public class ResolverActivity extends Activity { * * @return human-readable disambig string or null if none present */ - public CharSequence getExtendedInfo(); + CharSequence getExtendedInfo(); /** * @return The drawable that should be used to represent this target */ - public Drawable getDisplayIcon(); + Drawable getDisplayIcon(); /** * @return The (small) icon to badge the target with */ - public Drawable getBadgeIcon(); + Drawable getBadgeIcon(); /** * @return The content description for the badge icon */ - public CharSequence getBadgeContentDescription(); + CharSequence getBadgeContentDescription(); /** * Clone this target with the given fill-in information. */ - public TargetInfo cloneFilledIn(Intent fillInIntent, int flags); + TargetInfo cloneFilledIn(Intent fillInIntent, int flags); /** * @return the list of supported source intents deduped against this single target */ - public List getAllSourceIntents(); + List getAllSourceIntents(); + + /** + * @return true if this target should be pinned to the front by the request of the user + */ + boolean isPinned(); } - class ResolveListAdapter extends BaseAdapter { + public class ResolveListAdapter extends BaseAdapter { private final List mIntents; private final Intent[] mInitialIntents; private final List mBaseResolveList; @@ -1372,9 +1389,12 @@ public class ResolverActivity extends Activity { } } if (!found) { - into.add(new ResolvedComponentInfo(new ComponentName( - newInfo.activityInfo.packageName, newInfo.activityInfo.name), - intent, newInfo)); + final ComponentName name = new ComponentName( + newInfo.activityInfo.packageName, newInfo.activityInfo.name); + final ResolvedComponentInfo rci = new ResolvedComponentInfo(name, + intent, newInfo); + rci.setPinned(isComponentPinned(name)); + into.add(rci); } } } @@ -1450,6 +1470,8 @@ public class ResolverActivity extends Activity { final Intent replaceIntent = getReplacementIntent(add.activityInfo, intent); final DisplayResolveInfo dri = new DisplayResolveInfo(intent, add, roLabel, extraInfo, replaceIntent); + final ComponentInfo ci = add.getComponentInfo(); + dri.setPinned(rci.isPinned()); addResolveInfo(dri); if (replaceIntent == intent) { // Only add alternates if we didn't get a specific replacement from @@ -1533,11 +1555,11 @@ public class ResolverActivity extends Activity { return false; } - protected int getDisplayResolveInfoCount() { + public int getDisplayResolveInfoCount() { return mDisplayList.size(); } - protected DisplayResolveInfo getDisplayResolveInfo(int index) { + public DisplayResolveInfo getDisplayResolveInfo(int index) { // Used to query services. We only query services for primary targets, not alternates. return mDisplayList.get(index); } @@ -1567,6 +1589,10 @@ public class ResolverActivity extends Activity { return !TextUtils.isEmpty(info.getExtendedInfo()); } + public boolean isComponentPinned(ComponentName name) { + return false; + } + public final void bindView(int position, View view) { onBindView(view, getItem(position)); } @@ -1603,6 +1629,7 @@ public class ResolverActivity extends Activity { static final class ResolvedComponentInfo { public final ComponentName name; + private boolean mPinned; private final List mIntents = new ArrayList<>(); private final List mResolveInfos = new ArrayList<>(); @@ -1645,6 +1672,14 @@ public class ResolverActivity extends Activity { } return -1; } + + public boolean isPinned() { + return mPinned; + } + + public void setPinned(boolean pinned) { + mPinned = pinned; + } } static class ViewHolder { @@ -1698,7 +1733,7 @@ public class ResolverActivity extends Activity { return false; } ResolveInfo ri = mAdapter.resolveInfoForPosition(position, true); - showAppDetails(ri); + showTargetDetails(ri); return true; } diff --git a/core/java/com/android/internal/app/ResolverComparator.java b/core/java/com/android/internal/app/ResolverComparator.java index 31556e29bc32..964a7f53e357 100644 --- a/core/java/com/android/internal/app/ResolverComparator.java +++ b/core/java/com/android/internal/app/ResolverComparator.java @@ -48,7 +48,7 @@ class ResolverComparator implements Comparator { private static final boolean DEBUG = false; // Two weeks - private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 14; + private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7; private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12; @@ -171,15 +171,27 @@ class ResolverComparator implements Comparator { } } - if (mStats != null) { - final ScoredTarget lhsTarget = mScoredTargets.get(new ComponentName( - lhs.activityInfo.packageName, lhs.activityInfo.name)); - final ScoredTarget rhsTarget = mScoredTargets.get(new ComponentName( - rhs.activityInfo.packageName, rhs.activityInfo.name)); - final float diff = rhsTarget.score - lhsTarget.score; + final boolean lPinned = lhsp.isPinned(); + final boolean rPinned = rhsp.isPinned(); - if (diff != 0) { - return diff > 0 ? 1 : -1; + if (lPinned && !rPinned) { + return -1; + } else if (!lPinned && rPinned) { + return 1; + } + + // Pinned items stay stable within a normal lexical sort and ignore scoring. + if (!lPinned && !rPinned) { + if (mStats != null) { + final ScoredTarget lhsTarget = mScoredTargets.get(new ComponentName( + lhs.activityInfo.packageName, lhs.activityInfo.name)); + final ScoredTarget rhsTarget = mScoredTargets.get(new ComponentName( + rhs.activityInfo.packageName, rhs.activityInfo.name)); + final float diff = rhsTarget.score - lhsTarget.score; + + if (diff != 0) { + return diff > 0 ? 1 : -1; + } } } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 1c3db104dc3c..4ab81e990513 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2887,19 +2887,6 @@ android:theme="@style/Theme.Material.DayNight.DarkActionBar" android:forceDeviceEncrypted="true" android:encryptionAware="true"> - - - - - - - + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/arrays.xml b/packages/SystemUI/res/values/arrays.xml index bf0cba22ab0c..32025e06e5b9 100644 --- a/packages/SystemUI/res/values/arrays.xml +++ b/packages/SystemUI/res/values/arrays.xml @@ -51,4 +51,16 @@ 03 33 + + + + @string/pin_target + @string/app_info + + + + @string/unpin_target + @string/app_info + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index b09693444bdf..707225c6b7ea 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1338,4 +1338,9 @@ Data Saver is off + + Pin + Unpin + App info + diff --git a/packages/SystemUI/src/com/android/systemui/chooser/ChooserActivity.java b/packages/SystemUI/src/com/android/systemui/chooser/ChooserActivity.java new file mode 100644 index 000000000000..a8bc36e58e19 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/chooser/ChooserActivity.java @@ -0,0 +1,1467 @@ +/* + * 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.systemui.chooser; + +import android.animation.ObjectAnimator; +import android.annotation.NonNull; +import android.app.Activity; +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.SharedPreferences; +import android.content.pm.ActivityInfo; +import android.content.pm.LabeledIntent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.database.DataSetObserver; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +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.os.UserManager; +import android.provider.DocumentsContract; +import android.service.chooser.ChooserTarget; +import android.service.chooser.ChooserTargetService; +import android.service.chooser.IChooserTargetResult; +import android.service.chooser.IChooserTargetService; +import android.text.TextUtils; +import android.util.FloatProperty; +import android.util.Log; +import android.util.Slog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.MeasureSpec; +import android.view.View.OnClickListener; +import android.view.View.OnLongClickListener; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.AbsListView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import com.android.internal.R; +import com.android.internal.app.IntentForwarderActivity; +import com.android.internal.app.ResolverActivity; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.MetricsProto.MetricsEvent; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +public class ChooserActivity extends ResolverActivity { + private static final String TAG = "ChooserActivity"; + + private static final boolean DEBUG = false; + + 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 Intent mReferrerFillInIntent; + + private ChooserListAdapter mChooserListAdapter; + private ChooserRowAdapter mChooserRowAdapter; + + private SharedPreferences mPinnedSharedPrefs; + private static final float PINNED_TARGET_SCORE_BOOST = 1000.f; + private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings"; + private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; + + private final List mServiceConnections = new ArrayList<>(); + + private static final int CHOOSER_TARGET_SERVICE_RESULT = 1; + private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT = 2; + + private final Handler mChooserHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case CHOOSER_TARGET_SERVICE_RESULT: + if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_SERVICE_RESULT"); + if (isDestroyed()) break; + final ServiceResultInfo sri = (ServiceResultInfo) msg.obj; + if (!mServiceConnections.contains(sri.connection)) { + Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection + + " returned after being removed from active connections." + + " Have you considered returning results faster?"); + break; + } + if (sri.resultTargets != null) { + mChooserListAdapter.addServiceResults(sri.originalTarget, + sri.resultTargets); + } + unbindService(sri.connection); + sri.connection.destroy(); + mServiceConnections.remove(sri.connection); + if (mServiceConnections.isEmpty()) { + mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); + sendVoiceChoicesIfNeeded(); + } + break; + + case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT: + if (DEBUG) { + Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services"); + } + unbindRemainingServices(); + sendVoiceChoicesIfNeeded(); + break; + + default: + super.handleMessage(msg); + } + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Intent intent = getIntent(); + Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT); + if (!(targetParcelable instanceof Intent)) { + Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable); + finish(); + super.onCreate(null); + return; + } + Intent target = (Intent) targetParcelable; + 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; + if (title == null) { + defaultTitleRes = com.android.internal.R.string.chooseActivity; + } + Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS); + Intent[] initialIntents = null; + if (pa != null) { + initialIntents = new Intent[pa.length]; + for (int i=0; i 0 + && resolve.activityInfo != null && DocumentsContract.PACKAGE_DOCUMENTS_UI + .equals(resolve.activityInfo.packageName)) { + return true; + } + + return false; + } + + @Override + public void showTargetDetails(ResolveInfo ri) { + ComponentName name = ri.activityInfo.getComponentName(); + boolean pinned = mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); + ResolverTargetActionsDialogFragment f = + new ResolverTargetActionsDialogFragment(ri.loadLabel(getPackageManager()), + name, pinned); + f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG); + } + + private void modifyTargetIntent(Intent in) { + final String action = in.getAction(); + if (Intent.ACTION_SEND.equals(action) || + Intent.ACTION_SEND_MULTIPLE.equals(action)) { + in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT | + Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + } + } + + @Override + protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { + if (mRefinementIntentSender != null) { + final Intent fillIn = new Intent(); + final List 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); + } + + @Override + public void startSelected(int which, boolean always, boolean filtered) { + super.startSelected(which, always, filtered); + + if (mChooserListAdapter != null) { + // Log the index of which type of target the user picked. + // Lower values mean the ranking was better. + int cat = 0; + int value = which; + switch (mChooserListAdapter.getPositionTargetType(which)) { + case ChooserListAdapter.TARGET_CALLER: + cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET; + break; + case ChooserListAdapter.TARGET_SERVICE: + cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET; + value -= mChooserListAdapter.getCallerTargetCount(); + break; + case ChooserListAdapter.TARGET_STANDARD: + cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET; + value -= mChooserListAdapter.getCallerTargetCount() + + mChooserListAdapter.getServiceTargetCount(); + break; + } + + if (cat != 0) { + MetricsLogger.action(this, cat, value); + } + } + } + + void queryTargetServices(ChooserListAdapter adapter) { + final PackageManager pm = getPackageManager(); + int targetsToQuery = 0; + for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) { + final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i); + if (adapter.getScore(dri) == 0) { + // A score of 0 means the app hasn't been used in some time; + // don't query it as it's not likely to be relevant. + continue; + } + final ActivityInfo ai = dri.getResolveInfo().activityInfo; + final Bundle md = ai.metaData; + final String serviceName = md != null ? convertServiceName(ai.packageName, + md.getString(ChooserTargetService.META_DATA_NAME)) : null; + if (serviceName != null) { + final ComponentName serviceComponent = new ComponentName( + ai.packageName, serviceName); + final Intent serviceIntent = new Intent(ChooserTargetService.SERVICE_INTERFACE) + .setComponent(serviceComponent); + + if (DEBUG) { + Log.d(TAG, "queryTargets found target with service " + serviceComponent); + } + + try { + final String perm = pm.getServiceInfo(serviceComponent, 0).permission; + if (!ChooserTargetService.BIND_PERMISSION.equals(perm)) { + Log.w(TAG, "ChooserTargetService " + serviceComponent + " does not require" + + " permission " + ChooserTargetService.BIND_PERMISSION + + " - this service will not be queried for ChooserTargets." + + " add android:permission=\"" + + ChooserTargetService.BIND_PERMISSION + "\"" + + " to the tag for " + serviceComponent + + " in the manifest."); + continue; + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not look up service " + serviceComponent, e); + continue; + } + + final ChooserTargetServiceConnection conn = + new ChooserTargetServiceConnection(this, dri); + if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND, + UserHandle.CURRENT)) { + if (DEBUG) { + Log.d(TAG, "Binding service connection for target " + dri + + " intent " + serviceIntent); + } + mServiceConnections.add(conn); + targetsToQuery++; + } + } + if (targetsToQuery >= QUERY_TARGET_SERVICE_LIMIT) { + if (DEBUG) Log.d(TAG, "queryTargets hit query target limit " + + QUERY_TARGET_SERVICE_LIMIT); + break; + } + } + + if (!mServiceConnections.isEmpty()) { + if (DEBUG) Log.d(TAG, "queryTargets setting watchdog timer for " + + WATCHDOG_TIMEOUT_MILLIS + "ms"); + mChooserHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT, + WATCHDOG_TIMEOUT_MILLIS); + } else { + sendVoiceChoicesIfNeeded(); + } + } + + private String convertServiceName(String packageName, String serviceName) { + if (TextUtils.isEmpty(serviceName)) { + return null; + } + + final String fullName; + if (serviceName.startsWith(".")) { + // Relative to the app package. Prepend the app package name. + fullName = packageName + serviceName; + } else if (serviceName.indexOf('.') >= 0) { + // Fully qualified package name. + fullName = serviceName; + } else { + fullName = null; + } + return fullName; + } + + void unbindRemainingServices() { + if (DEBUG) { + Log.d(TAG, "unbindRemainingServices, " + mServiceConnections.size() + " left"); + } + for (int i = 0, N = mServiceConnections.size(); i < N; i++) { + final ChooserTargetServiceConnection conn = mServiceConnections.get(i); + if (DEBUG) Log.d(TAG, "unbinding " + conn); + unbindService(conn); + conn.destroy(); + } + mServiceConnections.clear(); + mChooserHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); + } + + public void onSetupVoiceInteraction() { + // Do nothing. We'll send the voice stuff ourselves. + } + + 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 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; + } + + void filterServiceTargets(String packageName, List targets) { + if (targets == null) { + return; + } + + final PackageManager pm = getPackageManager(); + for (int i = targets.size() - 1; i >= 0; i--) { + final ChooserTarget target = targets.get(i); + final ComponentName targetName = target.getComponentName(); + if (packageName != null && packageName.equals(targetName.getPackageName())) { + // Anything from the original target's package is fine. + continue; + } + + boolean remove; + try { + final ActivityInfo ai = pm.getActivityInfo(targetName, 0); + remove = !ai.exported || ai.permission != null; + } catch (NameNotFoundException e) { + Log.e(TAG, "Target " + target + " returned by " + packageName + + " component not found"); + remove = true; + } + + if (remove) { + targets.remove(i); + } + } + } + + @Override + public ResolveListAdapter createAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, int launchedFromUid, + boolean filterLastUsed) { + final ChooserListAdapter adapter = new ChooserListAdapter(context, payloadIntents, + initialIntents, rList, launchedFromUid, filterLastUsed); + if (DEBUG) Log.d(TAG, "Adapter created; querying services"); + queryTargetServices(adapter); + return adapter; + } + + final class ChooserTargetInfo implements TargetInfo { + private final DisplayResolveInfo mSourceInfo; + private final ResolveInfo mBackupResolveInfo; + private final ChooserTarget mChooserTarget; + private Drawable mBadgeIcon = null; + private CharSequence mBadgeContentDescription; + private Drawable mDisplayIcon; + private final Intent mFillInIntent; + private final int mFillInFlags; + private final float mModifiedScore; + + public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, + float modifiedScore) { + mSourceInfo = sourceInfo; + mChooserTarget = chooserTarget; + mModifiedScore = modifiedScore; + if (sourceInfo != null) { + final ResolveInfo ri = sourceInfo.getResolveInfo(); + if (ri != null) { + final ActivityInfo ai = ri.activityInfo; + if (ai != null && ai.applicationInfo != null) { + final PackageManager pm = getPackageManager(); + mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); + mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); + } + } + } + final Icon icon = chooserTarget.getIcon(); + // TODO do this in the background + mDisplayIcon = icon != null ? icon.loadDrawable(ChooserActivity.this) : null; + + if (sourceInfo != null) { + mBackupResolveInfo = null; + } 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; + mBadgeIcon = other.mBadgeIcon; + mBadgeContentDescription = other.mBadgeContentDescription; + mDisplayIcon = other.mDisplayIcon; + mFillInIntent = fillInIntent; + mFillInFlags = flags; + mModifiedScore = other.mModifiedScore; + } + + public float getModifiedScore() { + return mModifiedScore; + } + + @Override + public Intent getResolvedIntent() { + if (mSourceInfo != null) { + return mSourceInfo.getResolvedIntent(); + } + return getTargetIntent(); + } + + @Override + public ComponentName getResolvedComponentName() { + if (mSourceInfo != null) { + return mSourceInfo.getResolvedComponentName(); + } else if (mBackupResolveInfo != null) { + return new ComponentName(mBackupResolveInfo.activityInfo.packageName, + mBackupResolveInfo.activityInfo.name); + } + return null; + } + + private Intent getBaseIntentToSend() { + Intent result = mSourceInfo != null + ? mSourceInfo.getResolvedIntent() : getTargetIntent(); + if (result == null) { + Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); + } else { + result = new Intent(result); + if (mFillInIntent != null) { + result.fillIn(mFillInIntent, mFillInFlags); + } + result.fillIn(mReferrerFillInIntent, 0); + } + return result; + } + + @Override + public boolean start(Activity activity, Bundle options) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } + + @Override + public boolean startAsCaller(Activity activity, Bundle options, int userId) { + final Intent intent = getBaseIntentToSend(); + if (intent == null) { + return false; + } + intent.setComponent(mChooserTarget.getComponentName()); + intent.putExtras(mChooserTarget.getIntentExtras()); + activity.startActivityAsCaller(intent, options, true, userId); + return true; + } + + @Override + public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { + throw new RuntimeException("ChooserTargets should be started as caller."); + } + + @Override + public ResolveInfo getResolveInfo() { + return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; + } + + @Override + public CharSequence getDisplayLabel() { + return mChooserTarget.getTitle(); + } + + @Override + public CharSequence getExtendedInfo() { + // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. + return null; + } + + @Override + public Drawable getDisplayIcon() { + return mDisplayIcon; + } + + @Override + public Drawable getBadgeIcon() { + return mBadgeIcon; + } + + @Override + public CharSequence getBadgeContentDescription() { + return mBadgeContentDescription; + } + + @Override + public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { + return new ChooserTargetInfo(this, fillInIntent, flags); + } + + @Override + public List getAllSourceIntents() { + final List 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; + } + + @Override + public boolean isPinned() { + return mSourceInfo != null ? mSourceInfo.isPinned() : false; + } + } + + public class ChooserListAdapter extends ResolveListAdapter { + public static final int TARGET_BAD = -1; + public static final int TARGET_CALLER = 0; + public static final int TARGET_SERVICE = 1; + public static final int TARGET_STANDARD = 2; + + private static final int MAX_SERVICE_TARGETS = 8; + + private final List mServiceTargets = new ArrayList<>(); + private final List mCallerTargets = new ArrayList<>(); + + private float mLateFee = 1.f; + + private final BaseChooserTargetComparator mBaseTargetComparator + = new BaseChooserTargetComparator(); + + public ChooserListAdapter(Context context, List payloadIntents, + Intent[] initialIntents, List rList, int launchedFromUid, + boolean filterLastUsed) { + // Don't send the initial intents through the shared ResolverActivity path, + // we want to separate them into a different section. + super(context, payloadIntents, null, rList, launchedFromUid, filterLastUsed); + + if (initialIntents != null) { + final PackageManager pm = getPackageManager(); + for (int i = 0; i < initialIntents.length; i++) { + final Intent ii = initialIntents[i]; + if (ii == null) { + continue; + } + final ActivityInfo ai = ii.resolveActivityInfo(pm, 0); + if (ai == null) { + Log.w(TAG, "No activity found for " + ii); + continue; + } + ResolveInfo ri = new ResolveInfo(); + ri.activityInfo = ai; + UserManager userManager = + (UserManager) getSystemService(Context.USER_SERVICE); + if (ii instanceof LabeledIntent) { + LabeledIntent li = (LabeledIntent)ii; + ri.resolvePackageName = li.getSourcePackage(); + ri.labelRes = li.getLabelResource(); + ri.nonLocalizedLabel = li.getNonLocalizedLabel(); + ri.icon = li.getIconResource(); + ri.iconResourceId = ri.icon; + } + if (userManager.isManagedProfile()) { + ri.noResourceId = true; + ri.icon = 0; + } + mCallerTargets.add(new DisplayResolveInfo(ii, ri, + ri.loadLabel(pm), null, ii)); + } + } + } + + @Override + public boolean showsExtendedInfo(TargetInfo info) { + // We have badges so we don't need this text shown. + return false; + } + + @Override + public boolean isComponentPinned(ComponentName name) { + return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false); + } + + @Override + public float getScore(DisplayResolveInfo target) { + float score = super.getScore(target); + if (target.isPinned()) { + score += PINNED_TARGET_SCORE_BOOST; + } + return score; + } + + @Override + public View onCreateView(ViewGroup parent) { + return mInflater.inflate( + com.android.internal.R.layout.resolve_grid_item, parent, false); + } + + @Override + public void onListRebuilt() { + if (mServiceTargets != null) { + pruneServiceTargets(); + } + } + + @Override + public boolean shouldGetResolvedFilter() { + return true; + } + + @Override + public int getCount() { + return super.getCount() + getServiceTargetCount() + getCallerTargetCount(); + } + + @Override + public int getUnfilteredCount() { + return super.getUnfilteredCount() + getServiceTargetCount() + getCallerTargetCount(); + } + + public int getCallerTargetCount() { + return mCallerTargets.size(); + } + + public int getServiceTargetCount() { + return Math.min(mServiceTargets.size(), MAX_SERVICE_TARGETS); + } + + public int getStandardTargetCount() { + return super.getCount(); + } + + public int getPositionTargetType(int position) { + int offset = 0; + + final int callerTargetCount = getCallerTargetCount(); + if (position < callerTargetCount) { + return TARGET_CALLER; + } + offset += callerTargetCount; + + final int serviceTargetCount = getServiceTargetCount(); + if (position - offset < serviceTargetCount) { + return TARGET_SERVICE; + } + offset += serviceTargetCount; + + final int standardTargetCount = super.getCount(); + if (position - offset < standardTargetCount) { + return TARGET_STANDARD; + } + + return TARGET_BAD; + } + + @Override + public TargetInfo getItem(int position) { + return targetInfoForPosition(position, true); + } + + @Override + public TargetInfo targetInfoForPosition(int position, boolean filtered) { + int offset = 0; + + final int callerTargetCount = getCallerTargetCount(); + if (position < callerTargetCount) { + return mCallerTargets.get(position); + } + offset += callerTargetCount; + + final int serviceTargetCount = getServiceTargetCount(); + if (position - offset < serviceTargetCount) { + return mServiceTargets.get(position - offset); + } + offset += serviceTargetCount; + + return filtered ? super.getItem(position - offset) + : getDisplayInfoAt(position - offset); + } + + public void addServiceResults(DisplayResolveInfo origTarget, List targets) { + if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() + + " targets"); + final float parentScore = getScore(origTarget); + Collections.sort(targets, mBaseTargetComparator); + float lastScore = 0; + for (int i = 0, N = targets.size(); i < N; i++) { + final ChooserTarget target = targets.get(i); + float targetScore = target.getScore(); + targetScore *= parentScore; + targetScore *= mLateFee; + if (i > 0 && targetScore >= lastScore) { + // Apply a decay so that the top app can't crowd out everything else. + // This incents ChooserTargetServices to define what's truly better. + targetScore = lastScore * 0.95f; + } + insertServiceTarget(new ChooserTargetInfo(origTarget, target, targetScore)); + + if (DEBUG) { + Log.d(TAG, " => " + target.toString() + " score=" + targetScore + + " base=" + target.getScore() + + " lastScore=" + lastScore + + " parentScore=" + parentScore + + " lateFee=" + mLateFee); + } + + lastScore = targetScore; + } + + mLateFee *= 0.95f; + + notifyDataSetChanged(); + } + + private void insertServiceTarget(ChooserTargetInfo chooserTargetInfo) { + final float newScore = chooserTargetInfo.getModifiedScore(); + for (int i = 0, N = mServiceTargets.size(); i < N; i++) { + final ChooserTargetInfo serviceTarget = mServiceTargets.get(i); + if (newScore > serviceTarget.getModifiedScore()) { + mServiceTargets.add(i, chooserTargetInfo); + return; + } + } + mServiceTargets.add(chooserTargetInfo); + } + + private void pruneServiceTargets() { + if (DEBUG) Log.d(TAG, "pruneServiceTargets"); + for (int i = mServiceTargets.size() - 1; i >= 0; i--) { + final ChooserTargetInfo cti = mServiceTargets.get(i); + if (!hasResolvedTarget(cti.getResolveInfo())) { + if (DEBUG) Log.d(TAG, " => " + i + " " + cti); + mServiceTargets.remove(i); + } + } + } + } + + static class BaseChooserTargetComparator implements Comparator { + @Override + public int compare(ChooserTarget lhs, ChooserTarget rhs) { + // Descending order + return (int) Math.signum(rhs.getScore() - lhs.getScore()); + } + } + + static class RowScale { + private static final int DURATION = 400; + + float mScale; + ChooserRowAdapter mAdapter; + private final ObjectAnimator mAnimator; + + public static final FloatProperty PROPERTY = + new FloatProperty("scale") { + @Override + public void setValue(RowScale object, float value) { + object.mScale = value; + object.mAdapter.notifyDataSetChanged(); + } + + @Override + public Float get(RowScale object) { + return object.mScale; + } + }; + + public RowScale(@NonNull ChooserRowAdapter adapter, float from, float to) { + mAdapter = adapter; + mScale = from; + if (from == to) { + mAnimator = null; + return; + } + + mAnimator = ObjectAnimator.ofFloat(this, PROPERTY, from, to).setDuration(DURATION); + } + + public RowScale setInterpolator(Interpolator interpolator) { + if (mAnimator != null) { + mAnimator.setInterpolator(interpolator); + } + return this; + } + + public float get() { + return mScale; + } + + public void startAnimation() { + if (mAnimator != null) { + mAnimator.start(); + } + } + + public void cancelAnimation() { + if (mAnimator != null) { + mAnimator.cancel(); + } + } + } + + class ChooserRowAdapter extends BaseAdapter { + private ChooserListAdapter mChooserListAdapter; + private final LayoutInflater mLayoutInflater; + private final int mColumnCount = 4; + private RowScale[] mServiceTargetScale; + private final Interpolator mInterpolator; + + public ChooserRowAdapter(ChooserListAdapter wrappedAdapter) { + mChooserListAdapter = wrappedAdapter; + mLayoutInflater = LayoutInflater.from(ChooserActivity.this); + + mInterpolator = AnimationUtils.loadInterpolator(ChooserActivity.this, + android.R.interpolator.decelerate_quint); + + wrappedAdapter.registerDataSetObserver(new DataSetObserver() { + @Override + public void onChanged() { + super.onChanged(); + final int rcount = getServiceTargetRowCount(); + if (mServiceTargetScale == null + || mServiceTargetScale.length != rcount) { + RowScale[] old = mServiceTargetScale; + int oldRCount = old != null ? old.length : 0; + mServiceTargetScale = new RowScale[rcount]; + if (old != null && rcount > 0) { + System.arraycopy(old, 0, mServiceTargetScale, 0, + Math.min(old.length, rcount)); + } + + for (int i = rcount; i < oldRCount; i++) { + old[i].cancelAnimation(); + } + + for (int i = oldRCount; i < rcount; i++) { + final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f) + .setInterpolator(mInterpolator); + mServiceTargetScale[i] = rs; + } + + // Start the animations in a separate loop. + // The process of starting animations will result in + // binding views to set up initial values, and we must + // have ALL of the new RowScale objects created above before + // we get started. + for (int i = oldRCount; i < rcount; i++) { + mServiceTargetScale[i].startAnimation(); + } + } + + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + super.onInvalidated(); + notifyDataSetInvalidated(); + if (mServiceTargetScale != null) { + for (RowScale rs : mServiceTargetScale) { + rs.cancelAnimation(); + } + } + } + }); + } + + private float getRowScale(int rowPosition) { + final int start = getCallerTargetRowCount(); + final int end = start + getServiceTargetRowCount(); + if (rowPosition >= start && rowPosition < end) { + return mServiceTargetScale[rowPosition - start].get(); + } + return 1.f; + } + + @Override + public int getCount() { + return (int) ( + getCallerTargetRowCount() + + getServiceTargetRowCount() + + Math.ceil((float) mChooserListAdapter.getStandardTargetCount() / mColumnCount) + ); + } + + public int getCallerTargetRowCount() { + return (int) Math.ceil( + (float) mChooserListAdapter.getCallerTargetCount() / mColumnCount); + } + + public int getServiceTargetRowCount() { + return (int) Math.ceil( + (float) mChooserListAdapter.getServiceTargetCount() / mColumnCount); + } + + @Override + public Object getItem(int position) { + // We have nothing useful to return here. + return position; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final RowViewHolder holder; + if (convertView == null) { + holder = createViewHolder(parent); + } else { + holder = (RowViewHolder) convertView.getTag(); + } + bindViewHolder(position, holder); + + return holder.row; + } + + RowViewHolder createViewHolder(ViewGroup parent) { + final ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, + parent, false); + final RowViewHolder holder = new RowViewHolder(row, mColumnCount); + final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + for (int i = 0; i < mColumnCount; i++) { + final View v = mChooserListAdapter.createView(row); + final int column = i; + v.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startSelected(holder.itemIndices[column], false, true); + } + }); + v.setOnLongClickListener(new OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + showTargetDetails( + mChooserListAdapter.resolveInfoForPosition( + holder.itemIndices[column], true)); + return true; + } + }); + row.addView(v); + holder.cells[i] = v; + + // Force height to be a given so we don't have visual disruption during scaling. + LayoutParams lp = v.getLayoutParams(); + v.measure(spec, spec); + if (lp == null) { + lp = new LayoutParams(LayoutParams.MATCH_PARENT, v.getMeasuredHeight()); + row.setLayoutParams(lp); + } else { + lp.height = v.getMeasuredHeight(); + } + } + + // Pre-measure so we can scale later. + holder.measure(); + LayoutParams lp = row.getLayoutParams(); + if (lp == null) { + lp = new LayoutParams(LayoutParams.MATCH_PARENT, holder.measuredRowHeight); + row.setLayoutParams(lp); + } else { + lp.height = holder.measuredRowHeight; + } + row.setTag(holder); + return holder; + } + + void bindViewHolder(int rowPosition, RowViewHolder holder) { + final int start = getFirstRowPosition(rowPosition); + final int startType = mChooserListAdapter.getPositionTargetType(start); + + int end = start + mColumnCount - 1; + while (mChooserListAdapter.getPositionTargetType(end) != startType && end >= start) { + end--; + } + + if (startType == ChooserListAdapter.TARGET_SERVICE) { + holder.row.setBackgroundColor( + getColor(R.color.chooser_service_row_background_color)); + } else { + holder.row.setBackgroundColor(Color.TRANSPARENT); + } + + final int oldHeight = holder.row.getLayoutParams().height; + holder.row.getLayoutParams().height = Math.max(1, + (int) (holder.measuredRowHeight * getRowScale(rowPosition))); + if (holder.row.getLayoutParams().height != oldHeight) { + holder.row.requestLayout(); + } + + for (int i = 0; i < mColumnCount; i++) { + final View v = holder.cells[i]; + if (start + i <= end) { + v.setVisibility(View.VISIBLE); + holder.itemIndices[i] = start + i; + mChooserListAdapter.bindView(holder.itemIndices[i], v); + } else { + v.setVisibility(View.GONE); + } + } + } + + int getFirstRowPosition(int row) { + final int callerCount = mChooserListAdapter.getCallerTargetCount(); + final int callerRows = (int) Math.ceil((float) callerCount / mColumnCount); + + if (row < callerRows) { + return row * mColumnCount; + } + + final int serviceCount = mChooserListAdapter.getServiceTargetCount(); + final int serviceRows = (int) Math.ceil((float) serviceCount / mColumnCount); + + if (row < callerRows + serviceRows) { + return callerCount + (row - callerRows) * mColumnCount; + } + + return callerCount + serviceCount + + (row - callerRows - serviceRows) * mColumnCount; + } + } + + static class RowViewHolder { + final View[] cells; + final ViewGroup row; + int measuredRowHeight; + int[] itemIndices; + + public RowViewHolder(ViewGroup row, int cellCount) { + this.row = row; + this.cells = new View[cellCount]; + this.itemIndices = new int[cellCount]; + } + + public void measure() { + final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + row.measure(spec, spec); + measuredRowHeight = row.getMeasuredHeight(); + } + } + + static class ChooserTargetServiceConnection implements ServiceConnection { + private final DisplayResolveInfo mOriginalTarget; + private ComponentName mConnectedComponent; + private ChooserActivity mChooserActivity; + private final Object mLock = new Object(); + + private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() { + @Override + public void sendResult(List targets) throws RemoteException { + synchronized (mLock) { + if (mChooserActivity == null) { + Log.e(TAG, "destroyed ChooserTargetServiceConnection received result from " + + mConnectedComponent + "; ignoring..."); + return; + } + mChooserActivity.filterServiceTargets( + mOriginalTarget.getResolveInfo().activityInfo.packageName, targets); + final Message msg = Message.obtain(); + msg.what = CHOOSER_TARGET_SERVICE_RESULT; + msg.obj = new ServiceResultInfo(mOriginalTarget, targets, + ChooserTargetServiceConnection.this); + mChooserActivity.mChooserHandler.sendMessage(msg); + } + } + }; + + public ChooserTargetServiceConnection(ChooserActivity chooserActivity, + DisplayResolveInfo dri) { + mChooserActivity = chooserActivity; + mOriginalTarget = dri; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (DEBUG) Log.d(TAG, "onServiceConnected: " + name); + synchronized (mLock) { + if (mChooserActivity == null) { + Log.e(TAG, "destroyed ChooserTargetServiceConnection got onServiceConnected"); + return; + } + + final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service); + try { + icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(), + mOriginalTarget.getResolveInfo().filter, mChooserTargetResult); + } catch (RemoteException e) { + Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e); + mChooserActivity.unbindService(this); + destroy(); + mChooserActivity.mServiceConnections.remove(this); + } + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name); + synchronized (mLock) { + if (mChooserActivity == null) { + Log.e(TAG, + "destroyed ChooserTargetServiceConnection got onServiceDisconnected"); + return; + } + + mChooserActivity.unbindService(this); + destroy(); + mChooserActivity.mServiceConnections.remove(this); + if (mChooserActivity.mServiceConnections.isEmpty()) { + mChooserActivity.mChooserHandler.removeMessages( + CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); + mChooserActivity.sendVoiceChoicesIfNeeded(); + } + mConnectedComponent = null; + } + } + + public void destroy() { + synchronized (mLock) { + mChooserActivity = null; + } + } + + @Override + public String toString() { + return "ChooserTargetServiceConnection{service=" + + mConnectedComponent + ", activity=" + + mOriginalTarget.getResolveInfo().activityInfo.toString() + "}"; + } + } + + static class ServiceResultInfo { + public final DisplayResolveInfo originalTarget; + public final List resultTargets; + public final ChooserTargetServiceConnection connection; + + public ServiceResultInfo(DisplayResolveInfo ot, List rt, + ChooserTargetServiceConnection c) { + originalTarget = ot; + resultTargets = rt; + 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; + } + } + + class OffsetDataSetObserver extends DataSetObserver { + private final AbsListView mListView; + private int mCachedViewType = -1; + private View mCachedView; + + public OffsetDataSetObserver(AbsListView listView) { + mListView = listView; + } + + @Override + public void onChanged() { + if (mResolverDrawerLayout == null) { + return; + } + + final int chooserTargetRows = mChooserRowAdapter.getServiceTargetRowCount(); + int offset = 0; + for (int i = 0; i < chooserTargetRows; i++) { + final int pos = mChooserRowAdapter.getCallerTargetRowCount() + i; + final int vt = mChooserRowAdapter.getItemViewType(pos); + if (vt != mCachedViewType) { + mCachedView = null; + } + final View v = mChooserRowAdapter.getView(pos, mCachedView, mListView); + int height = ((RowViewHolder) (v.getTag())).measuredRowHeight; + + offset += (int) (height * mChooserRowAdapter.getRowScale(pos)); + + if (vt >= 0) { + mCachedViewType = vt; + mCachedView = v; + } else { + mCachedViewType = -1; + } + } + + mResolverDrawerLayout.setCollapsibleHeightReserved(offset); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/chooser/ResolverTargetActionsDialogFragment.java b/packages/SystemUI/src/com/android/systemui/chooser/ResolverTargetActionsDialogFragment.java new file mode 100644 index 000000000000..839aa4556450 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/chooser/ResolverTargetActionsDialogFragment.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES 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.systemui.chooser; + +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.ComponentName; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +import com.android.systemui.R; + +/** + * Shows a dialog with actions to take on a chooser target + */ +public class ResolverTargetActionsDialogFragment extends DialogFragment + implements DialogInterface.OnClickListener { + private static final String NAME_KEY = "componentName"; + private static final String PINNED_KEY = "pinned"; + private static final String TITLE_KEY = "title"; + + // Sync with R.array.resolver_target_actions_* resources + private static final int TOGGLE_PIN_INDEX = 0; + private static final int APP_INFO_INDEX = 1; + + public ResolverTargetActionsDialogFragment() { + } + + public ResolverTargetActionsDialogFragment(CharSequence title, ComponentName name, + boolean pinned) { + Bundle args = new Bundle(); + args.putCharSequence(TITLE_KEY, title); + args.putParcelable(NAME_KEY, name); + args.putBoolean(PINNED_KEY, pinned); + setArguments(args); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + final Bundle args = getArguments(); + final int itemRes = args.getBoolean(PINNED_KEY, false) + ? R.array.resolver_target_actions_unpin + : R.array.resolver_target_actions_pin; + return new Builder(getContext()) + .setCancelable(true) + .setItems(itemRes, this) + .setTitle(args.getCharSequence(TITLE_KEY)) + .create(); + } + + @Override + public void onClick(DialogInterface dialog, int which) { + final Bundle args = getArguments(); + ComponentName name = args.getParcelable(NAME_KEY); + switch (which) { + case TOGGLE_PIN_INDEX: + SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext()); + final String key = name.flattenToString(); + boolean currentVal = sp.getBoolean(name.flattenToString(), false); + if (currentVal) { + sp.edit().remove(key).apply(); + } else { + sp.edit().putBoolean(key, true).apply(); + } + + // Force the chooser to requery and resort things + getActivity().recreate(); + break; + case APP_INFO_INDEX: + Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", name.getPackageName(), null)) + .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT); + startActivity(in); + break; + } + dismiss(); + } +} -- cgit v1.2.3-59-g8ed1b