ChooserActivity.java revision d974c7b4d150661562e341eb743986f06d150298
1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.app;
18
19import android.app.Activity;
20import android.content.ComponentName;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentSender;
24import android.content.ServiceConnection;
25import android.content.pm.ActivityInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.PackageManager.NameNotFoundException;
28import android.content.pm.ResolveInfo;
29import android.graphics.drawable.BitmapDrawable;
30import android.graphics.drawable.Drawable;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.Message;
35import android.os.Parcelable;
36import android.os.RemoteException;
37import android.os.UserHandle;
38import android.service.chooser.ChooserTarget;
39import android.service.chooser.ChooserTargetService;
40import android.service.chooser.IChooserTargetResult;
41import android.service.chooser.IChooserTargetService;
42import android.text.TextUtils;
43import android.util.Log;
44import android.util.Slog;
45import android.view.View;
46import android.view.ViewGroup;
47
48import java.util.ArrayList;
49import java.util.List;
50
51public class ChooserActivity extends ResolverActivity {
52    private static final String TAG = "ChooserActivity";
53
54    private static final boolean DEBUG = false;
55
56    private static final int QUERY_TARGET_LIMIT = 5;
57    private static final int WATCHDOG_TIMEOUT_MILLIS = 5000;
58
59    private Bundle mReplacementExtras;
60    private IntentSender mChosenComponentSender;
61
62    private ChooserTarget[] mCallerChooserTargets;
63
64    private final List<ChooserTargetServiceConnection> mServiceConnections = new ArrayList<>();
65
66    private static final int CHOOSER_TARGET_SERVICE_RESULT = 1;
67    private static final int CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT = 2;
68
69    private Handler mTargetResultHandler = new Handler() {
70        @Override
71        public void handleMessage(Message msg) {
72            switch (msg.what) {
73                case CHOOSER_TARGET_SERVICE_RESULT:
74                    if (DEBUG) Log.d(TAG, "CHOOSER_TARGET_SERVICE_RESULT");
75                    if (isDestroyed()) break;
76                    final ServiceResultInfo sri = (ServiceResultInfo) msg.obj;
77                    if (!mServiceConnections.contains(sri.connection)) {
78                        Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection
79                                + " returned after being removed from active connections."
80                                + " Have you considered returning results faster?");
81                        break;
82                    }
83                    final ChooserListAdapter cla = (ChooserListAdapter) getAdapter();
84                    cla.addServiceResults(sri.originalTarget, sri.resultTargets);
85                    unbindService(sri.connection);
86                    mServiceConnections.remove(sri.connection);
87                    break;
88
89                case CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT:
90                    if (DEBUG) {
91                        Log.d(TAG, "CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT; unbinding services");
92                    }
93                    unbindRemainingServices();
94                    break;
95
96                default:
97                    super.handleMessage(msg);
98            }
99        }
100    };
101
102    @Override
103    protected void onCreate(Bundle savedInstanceState) {
104        Intent intent = getIntent();
105        Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
106        if (!(targetParcelable instanceof Intent)) {
107            Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable);
108            finish();
109            super.onCreate(null);
110            return;
111        }
112        Intent target = (Intent) targetParcelable;
113        if (target != null) {
114            modifyTargetIntent(target);
115        }
116        mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
117        CharSequence title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE);
118        int defaultTitleRes = 0;
119        if (title == null) {
120            defaultTitleRes = com.android.internal.R.string.chooseActivity;
121        }
122        Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS);
123        Intent[] initialIntents = null;
124        if (pa != null) {
125            initialIntents = new Intent[pa.length];
126            for (int i=0; i<pa.length; i++) {
127                if (!(pa[i] instanceof Intent)) {
128                    Log.w("ChooserActivity", "Initial intent #" + i + " not an Intent: " + pa[i]);
129                    finish();
130                    super.onCreate(null);
131                    return;
132                }
133                final Intent in = (Intent) pa[i];
134                modifyTargetIntent(in);
135                initialIntents[i] = in;
136            }
137        }
138
139        pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
140        if (pa != null) {
141            final ChooserTarget[] targets = new ChooserTarget[pa.length];
142            for (int i = 0; i < pa.length; i++) {
143                if (!(pa[i] instanceof ChooserTarget)) {
144                    Log.w("ChooserActivity", "Chooser target #" + i + " is not a ChooserTarget: " +
145                            pa[i]);
146                    finish();
147                    super.onCreate(null);
148                    return;
149                }
150                targets[i] = (ChooserTarget) pa[i];
151            }
152            mCallerChooserTargets = targets;
153        }
154        mChosenComponentSender = intent.getParcelableExtra(
155                Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
156        setSafeForwardingMode(true);
157        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
158                null, false);
159    }
160
161    @Override
162    public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
163        Intent result = defIntent;
164        if (mReplacementExtras != null) {
165            final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName);
166            if (replExtras != null) {
167                result = new Intent(defIntent);
168                result.putExtras(replExtras);
169            }
170        }
171        if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_USER_OWNER)
172                || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
173            result = Intent.createChooser(result,
174                    getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
175        }
176        return result;
177    }
178
179    @Override
180    void onActivityStarted(TargetInfo cti) {
181        if (mChosenComponentSender != null) {
182            final ComponentName target = cti.getResolvedComponentName();
183            if (target != null) {
184                final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
185                try {
186                    mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null);
187                } catch (IntentSender.SendIntentException e) {
188                    Slog.e(TAG, "Unable to launch supplied IntentSender to report "
189                            + "the chosen component: " + e);
190                }
191            }
192        }
193    }
194
195    @Override
196    int getLayoutResource() {
197        return com.android.internal.R.layout.chooser_grid;
198    }
199
200    @Override
201    boolean shouldGetActivityMetadata() {
202        return true;
203    }
204
205    private void modifyTargetIntent(Intent in) {
206        final String action = in.getAction();
207        if (Intent.ACTION_SEND.equals(action) ||
208                Intent.ACTION_SEND_MULTIPLE.equals(action)) {
209            in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
210                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
211        }
212    }
213
214    void queryTargetServices(ChooserListAdapter adapter) {
215        final PackageManager pm = getPackageManager();
216        int targetsToQuery = 0;
217        for (int i = 0, N = adapter.getDisplayResolveInfoCount(); i < N; i++) {
218            final DisplayResolveInfo dri = adapter.getDisplayResolveInfo(i);
219            final ActivityInfo ai = dri.getResolveInfo().activityInfo;
220            final Bundle md = ai.metaData;
221            final String serviceName = md != null ? convertServiceName(ai.packageName,
222                    md.getString(ChooserTargetService.META_DATA_NAME)) : null;
223            if (serviceName != null) {
224                final ComponentName serviceComponent = new ComponentName(
225                        ai.packageName, serviceName);
226                final Intent serviceIntent = new Intent(ChooserTargetService.SERVICE_INTERFACE)
227                        .setComponent(serviceComponent);
228
229                if (DEBUG) {
230                    Log.d(TAG, "queryTargets found target with service " + serviceComponent);
231                }
232
233                try {
234                    final String perm = pm.getServiceInfo(serviceComponent, 0).permission;
235                    if (!ChooserTargetService.BIND_PERMISSION.equals(perm)) {
236                        Log.w(TAG, "ChooserTargetService " + serviceComponent + " does not require"
237                                + " permission " + ChooserTargetService.BIND_PERMISSION
238                                + " - this service will not be queried for ChooserTargets."
239                                + " add android:permission=\""
240                                + ChooserTargetService.BIND_PERMISSION + "\""
241                                + " to the <service> tag for " + serviceComponent
242                                + " in the manifest.");
243                        continue;
244                    }
245                } catch (NameNotFoundException e) {
246                    Log.e(TAG, "Could not look up service " + serviceComponent, e);
247                    continue;
248                }
249
250                final ChooserTargetServiceConnection conn = new ChooserTargetServiceConnection(dri);
251                if (bindServiceAsUser(serviceIntent, conn, BIND_AUTO_CREATE | BIND_NOT_FOREGROUND,
252                        UserHandle.CURRENT)) {
253                    if (DEBUG) {
254                        Log.d(TAG, "Binding service connection for target " + dri
255                                + " intent " + serviceIntent);
256                    }
257                    mServiceConnections.add(conn);
258                    targetsToQuery++;
259                }
260            }
261            if (targetsToQuery >= QUERY_TARGET_LIMIT) {
262                if (DEBUG) Log.d(TAG, "queryTargets hit query target limit " + QUERY_TARGET_LIMIT);
263                break;
264            }
265        }
266
267        if (!mServiceConnections.isEmpty()) {
268            if (DEBUG) Log.d(TAG, "queryTargets setting watchdog timer for "
269                    + WATCHDOG_TIMEOUT_MILLIS + "ms");
270            mTargetResultHandler.sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT,
271                    WATCHDOG_TIMEOUT_MILLIS);
272        }
273    }
274
275    private String convertServiceName(String packageName, String serviceName) {
276        if (TextUtils.isEmpty(serviceName)) {
277            return null;
278        }
279
280        final String fullName;
281        if (serviceName.startsWith(".")) {
282            // Relative to the app package. Prepend the app package name.
283            fullName = packageName + serviceName;
284        } else if (serviceName.indexOf('.') >= 0) {
285            // Fully qualified package name.
286            fullName = serviceName;
287        } else {
288            fullName = null;
289        }
290        return fullName;
291    }
292
293    void unbindRemainingServices() {
294        if (DEBUG) {
295            Log.d(TAG, "unbindRemainingServices, " + mServiceConnections.size() + " left");
296        }
297        for (int i = 0, N = mServiceConnections.size(); i < N; i++) {
298            final ChooserTargetServiceConnection conn = mServiceConnections.get(i);
299            if (DEBUG) Log.d(TAG, "unbinding " + conn);
300            unbindService(conn);
301        }
302        mServiceConnections.clear();
303        mTargetResultHandler.removeMessages(CHOOSER_TARGET_SERVICE_WATCHDOG_TIMEOUT);
304    }
305
306    @Override
307    ResolveListAdapter createAdapter(Context context, Intent[] initialIntents,
308            List<ResolveInfo> rList, int launchedFromUid, boolean filterLastUsed) {
309        final ChooserListAdapter adapter = new ChooserListAdapter(context, initialIntents, rList,
310                launchedFromUid, filterLastUsed, mCallerChooserTargets);
311        if (DEBUG) Log.d(TAG, "Adapter created; querying services");
312        queryTargetServices(adapter);
313        return adapter;
314    }
315
316    class ChooserTargetInfo implements TargetInfo {
317        private final TargetInfo mSourceInfo;
318        private final ResolveInfo mBackupResolveInfo;
319        private final ChooserTarget mChooserTarget;
320        private final Drawable mDisplayIcon;
321
322        public ChooserTargetInfo(ChooserTarget target) {
323            this(null, target);
324        }
325
326        public ChooserTargetInfo(TargetInfo sourceInfo, ChooserTarget chooserTarget) {
327            mSourceInfo = sourceInfo;
328            mChooserTarget = chooserTarget;
329            mDisplayIcon = new BitmapDrawable(getResources(), chooserTarget.getIcon());
330
331            if (sourceInfo != null) {
332                mBackupResolveInfo = null;
333            } else {
334                mBackupResolveInfo = getPackageManager().resolveActivity(getResolvedIntent(), 0);
335            }
336        }
337
338        @Override
339        public Intent getResolvedIntent() {
340            final Intent targetIntent = mChooserTarget.getIntent();
341            if (targetIntent != null) {
342                return targetIntent;
343            } else if (mSourceInfo != null) {
344                return mSourceInfo.getResolvedIntent();
345            }
346            return getTargetIntent();
347        }
348
349        @Override
350        public ComponentName getResolvedComponentName() {
351            if (mSourceInfo != null) {
352                return mSourceInfo.getResolvedComponentName();
353            } else if (mBackupResolveInfo != null) {
354                return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
355                        mBackupResolveInfo.activityInfo.name);
356            }
357            return null;
358        }
359
360        private Intent getFillInIntent() {
361            return mSourceInfo != null ? mSourceInfo.getResolvedIntent() : getTargetIntent();
362        }
363
364        @Override
365        public boolean start(Activity activity, Bundle options) {
366            return mChooserTarget.sendIntent(activity, getFillInIntent());
367        }
368
369        @Override
370        public boolean startAsCaller(Activity activity, Bundle options, int userId) {
371            return mChooserTarget.sendIntentAsCaller(activity, getFillInIntent(), userId);
372        }
373
374        @Override
375        public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
376            return mChooserTarget.sendIntentAsUser(activity, getFillInIntent(), user);
377        }
378
379        @Override
380        public ResolveInfo getResolveInfo() {
381            return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
382        }
383
384        @Override
385        public CharSequence getDisplayLabel() {
386            return mChooserTarget.getTitle();
387        }
388
389        @Override
390        public CharSequence getExtendedInfo() {
391            return mSourceInfo != null ? mSourceInfo.getExtendedInfo() : null;
392        }
393
394        @Override
395        public Drawable getDisplayIcon() {
396            return mDisplayIcon;
397        }
398    }
399
400    public class ChooserListAdapter extends ResolveListAdapter {
401        private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
402        private final List<ChooserTargetInfo> mCallerTargets = new ArrayList<>();
403
404        public ChooserListAdapter(Context context, Intent[] initialIntents, List<ResolveInfo> rList,
405                int launchedFromUid, boolean filterLastUsed, ChooserTarget[] callerChooserTargets) {
406            super(context, initialIntents, rList, launchedFromUid, filterLastUsed);
407
408            if (callerChooserTargets != null) {
409                for (ChooserTarget target : callerChooserTargets) {
410                    mCallerTargets.add(new ChooserTargetInfo(target));
411                }
412            }
413        }
414
415        @Override
416        public boolean showsExtendedInfo(TargetInfo info) {
417            // Reserve space to show extended info if any one of the items in the adapter has
418            // extended info. This keeps grid item sizes uniform.
419            return hasExtendedInfo();
420        }
421
422        @Override
423        public View createView(ViewGroup parent) {
424            return mInflater.inflate(
425                    com.android.internal.R.layout.resolve_grid_item, parent, false);
426        }
427
428        @Override
429        public void onListRebuilt() {
430            if (mServiceTargets != null) {
431                pruneServiceTargets();
432            }
433        }
434
435        @Override
436        public boolean shouldGetResolvedFilter() {
437            return true;
438        }
439
440        @Override
441        public int getCount() {
442            return super.getCount() + mServiceTargets.size() + mCallerTargets.size();
443        }
444
445        @Override
446        public TargetInfo getItem(int position) {
447            int offset = 0;
448
449            final int callerTargetCount = mCallerTargets.size();
450            if (position < callerTargetCount) {
451                return mCallerTargets.get(position);
452            }
453            offset += callerTargetCount;
454
455            final int serviceTargetCount = mServiceTargets.size();
456            if (position - offset < serviceTargetCount) {
457                return mServiceTargets.get(position - offset);
458            }
459            offset += serviceTargetCount;
460
461            return super.getItem(position - offset);
462        }
463
464        public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets) {
465            if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
466                    + " targets");
467            for (int i = 0, N = targets.size(); i < N; i++) {
468                mServiceTargets.add(new ChooserTargetInfo(origTarget, targets.get(i)));
469            }
470
471            // TODO: Maintain sort by ranking scores.
472
473            notifyDataSetChanged();
474        }
475
476        private void pruneServiceTargets() {
477            if (DEBUG) Log.d(TAG, "pruneServiceTargets");
478            for (int i = mServiceTargets.size() - 1; i >= 0; i--) {
479                final ChooserTargetInfo cti = mServiceTargets.get(i);
480                if (!hasResolvedTarget(cti.getResolveInfo())) {
481                    if (DEBUG) Log.d(TAG, " => " + i + " " + cti);
482                    mServiceTargets.remove(i);
483                }
484            }
485        }
486    }
487
488    class ChooserTargetServiceConnection implements ServiceConnection {
489        private final DisplayResolveInfo mOriginalTarget;
490
491        private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() {
492            @Override
493            public void sendResult(List<ChooserTarget> targets) throws RemoteException {
494                final Message msg = Message.obtain();
495                msg.what = CHOOSER_TARGET_SERVICE_RESULT;
496                msg.obj = new ServiceResultInfo(mOriginalTarget, targets,
497                        ChooserTargetServiceConnection.this);
498                mTargetResultHandler.sendMessage(msg);
499            }
500        };
501
502        public ChooserTargetServiceConnection(DisplayResolveInfo dri) {
503            mOriginalTarget = dri;
504        }
505
506        @Override
507        public void onServiceConnected(ComponentName name, IBinder service) {
508            if (DEBUG) Log.d(TAG, "onServiceConnected: " + name);
509            final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service);
510            try {
511                icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(),
512                        mOriginalTarget.getResolveInfo().filter, mChooserTargetResult);
513            } catch (RemoteException e) {
514                Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e);
515                unbindService(this);
516                mServiceConnections.remove(this);
517            }
518        }
519
520        @Override
521        public void onServiceDisconnected(ComponentName name) {
522            if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name);
523            unbindService(this);
524            mServiceConnections.remove(this);
525        }
526
527        @Override
528        public String toString() {
529            return mOriginalTarget.getResolveInfo().activityInfo.toString();
530        }
531    }
532
533    static class ServiceResultInfo {
534        public final DisplayResolveInfo originalTarget;
535        public final List<ChooserTarget> resultTargets;
536        public final ChooserTargetServiceConnection connection;
537
538        public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
539                ChooserTargetServiceConnection c) {
540            originalTarget = ot;
541            resultTargets = rt;
542            connection = c;
543        }
544    }
545}
546