/* * 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.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.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; 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.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.Log; import android.util.Slog; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; 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 ChooserTarget[] mCallerChooserTargets; 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 Handler mTargetResultHandler = 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; } final ChooserListAdapter cla = (ChooserListAdapter) getAdapter(); cla.addServiceResults(sri.originalTarget, sri.resultTargets); unbindService(sri.connection); mServiceConnections.remove(sri.connection); break; case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT: if (DEBUG) { Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services"); } unbindRemainingServices(); 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 sourceIntents = target.getAllSourceIntents(); if (!sourceIntents.isEmpty()) { fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); if (sourceIntents.size() > 1) { final Intent[] alts = new Intent[sourceIntents.size() - 1]; for (int i = 1, N = sourceIntents.size(); i < N; i++) { alts[i - 1] = sourceIntents.get(i); } fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts); } if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); } mRefinementResultReceiver = new RefinementResultReceiver(this, target, null); fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, mRefinementResultReceiver); try { mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null); return false; } catch (SendIntentException e) { Log.e(TAG, "Refinement IntentSender failed to send", e); } } } return super.onTargetSelected(target, alwaysCheck); } void queryTargetServices(ChooserListAdapter adapter) { final PackageManager pm = getPackageManager(); int targetsToQuery = 0; for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) { final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i); 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(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"); mTargetResultHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT, WATCHDOG_TIMEOUT_MILLIS); } } 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); } mServiceConnections.clear(); mTargetResultHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT); } void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) { if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); mRefinementResultReceiver = null; } if (selectedTarget == null) { Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) { Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget + " cannot match refined source intent " + matchingIntent); } else if (super.onTargetSelected(selectedTarget.cloneFilledIn(matchingIntent, 0), false)) { finish(); return; } onRefinementCanceled(); } void onRefinementCanceled() { if (mRefinementResultReceiver != null) { mRefinementResultReceiver.destroy(); mRefinementResultReceiver = null; } finish(); } boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) { final List targetIntents = target.getAllSourceIntents(); for (int i = 0, N = targetIntents.size(); i < N; i++) { final Intent targetIntent = targetIntents.get(i); if (targetIntent.filterEquals(matchingIntent)) { return true; } } return false; } @Override ResolveListAdapter createAdapter(Context context, Intent[] initialIntents, List rList, int launchedFromUid, boolean filterLastUsed) { final ChooserListAdapter adapter = new ChooserListAdapter(context, initialIntents, rList, launchedFromUid, filterLastUsed, mCallerChooserTargets); 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 final Drawable mDisplayIcon; private final Intent mFillInIntent; private final int mFillInFlags; public ChooserTargetInfo(ChooserTarget target) { this(null, target); } public ChooserTargetInfo(DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget) { mSourceInfo = sourceInfo; mChooserTarget = chooserTarget; mDisplayIcon = new BitmapDrawable(getResources(), chooserTarget.getIcon()); 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; mDisplayIcon = other.mDisplayIcon; mFillInIntent = fillInIntent; mFillInFlags = flags; } @Override public Intent getResolvedIntent() { final Intent targetIntent = mChooserTarget.getIntent(); if (targetIntent != null) { return targetIntent; } else 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 getFillInIntent() { Intent result = mSourceInfo != null ? mSourceInfo.getResolvedIntent() : getTargetIntent(); if (result == null) { Log.e(TAG, "ChooserTargetInfo#getFillInIntent: no fillIn intent available"); } else if (mFillInIntent != null) { result = new Intent(result); result.fillIn(mFillInIntent, mFillInFlags); } return result; } @Override public boolean start(Activity activity, Bundle options) { final Intent intent = getFillInIntent(); if (intent == null) { return false; } return mChooserTarget.sendIntent(activity, intent); } @Override public boolean startAsCaller(Activity activity, Bundle options, int userId) { final Intent intent = getFillInIntent(); if (intent == null) { return false; } return mChooserTarget.sendIntentAsCaller(activity, intent, userId); } @Override public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { final Intent intent = getFillInIntent(); if (intent == null) { return false; } return mChooserTarget.sendIntentAsUser(activity, intent, user); } @Override public ResolveInfo getResolveInfo() { return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; } @Override public CharSequence getDisplayLabel() { return mChooserTarget.getTitle(); } @Override public CharSequence getExtendedInfo() { return mSourceInfo != null ? mSourceInfo.getExtendedInfo() : null; } @Override public Drawable getDisplayIcon() { return mDisplayIcon; } @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 { private final List mServiceTargets = new ArrayList<>(); private final List mCallerTargets = new ArrayList<>(); public ChooserListAdapter(Context context, Intent[] initialIntents, List rList, int launchedFromUid, boolean filterLastUsed, ChooserTarget[] callerChooserTargets) { super(context, initialIntents, rList, launchedFromUid, filterLastUsed); if (callerChooserTargets != null) { for (ChooserTarget target : callerChooserTargets) { mCallerTargets.add(new ChooserTargetInfo(target)); } } } @Override public boolean showsExtendedInfo(TargetInfo info) { // Reserve space to show extended info if any one of the items in the adapter has // extended info. This keeps grid item sizes uniform. return hasExtendedInfo(); } @Override public View createView(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() + mServiceTargets.size() + mCallerTargets.size(); } @Override public TargetInfo getItem(int position) { int offset = 0; final int callerTargetCount = mCallerTargets.size(); if (position < callerTargetCount) { return mCallerTargets.get(position); } offset += callerTargetCount; final int serviceTargetCount = mServiceTargets.size(); if (position - offset < serviceTargetCount) { return mServiceTargets.get(position - offset); } offset += serviceTargetCount; return super.getItem(position - offset); } public void addServiceResults(DisplayResolveInfo origTarget, List targets) { if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size() + " targets"); for (int i = 0, N = targets.size(); i < N; i++) { mServiceTargets.add(new ChooserTargetInfo(origTarget, targets.get(i))); } // TODO: Maintain sort by ranking scores. notifyDataSetChanged(); } 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); } } } } class ChooserTargetServiceConnection implements ServiceConnection { private final DisplayResolveInfo mOriginalTarget; private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() { @Override public void sendResult(List targets) throws RemoteException { final Message msg = Message.obtain(); msg.what = CHOOSER_TARGET_SERVICE_RESULT; msg.obj = new ServiceResultInfo(mOriginalTarget, targets, ChooserTargetServiceConnection.this); mTargetResultHandler.sendMessage(msg); } }; public ChooserTargetServiceConnection(DisplayResolveInfo dri) { mOriginalTarget = dri; } @Override public void onServiceConnected(ComponentName name, IBinder service) { if (DEBUG) Log.d(TAG, "onServiceConnected: " + name); 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); unbindService(this); mServiceConnections.remove(this); } } @Override public void onServiceDisconnected(ComponentName name) { if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name); unbindService(this); mServiceConnections.remove(this); } @Override public String toString() { return 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; } } }