ChooserActivity.java revision c6d5e3a406c0e80638304980bac13abaa703a9a0
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);
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 ChooserTarget mChooserTarget;
319        private final Drawable mDisplayIcon;
320
321        public ChooserTargetInfo(TargetInfo sourceInfo, ChooserTarget chooserTarget) {
322            mSourceInfo = sourceInfo;
323            mChooserTarget = chooserTarget;
324            mDisplayIcon = new BitmapDrawable(getResources(), chooserTarget.getIcon());
325        }
326
327        @Override
328        public Intent getResolvedIntent() {
329            final Intent targetIntent = mChooserTarget.getIntent();
330            return targetIntent != null ? targetIntent : mSourceInfo.getResolvedIntent();
331        }
332
333        @Override
334        public ComponentName getResolvedComponentName() {
335            return mSourceInfo.getResolvedComponentName();
336        }
337
338        @Override
339        public boolean start(Activity activity, Bundle options) {
340            return mChooserTarget.sendIntent(activity, mSourceInfo.getResolvedIntent());
341        }
342
343        @Override
344        public boolean startAsCaller(Activity activity, Bundle options, int userId) {
345            return mChooserTarget.sendIntentAsCaller(activity, mSourceInfo.getResolvedIntent(),
346                    userId);
347        }
348
349        @Override
350        public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
351            return mChooserTarget.sendIntentAsUser(activity, mSourceInfo.getResolvedIntent(), user);
352        }
353
354        @Override
355        public ResolveInfo getResolveInfo() {
356            return mSourceInfo.getResolveInfo();
357        }
358
359        @Override
360        public CharSequence getDisplayLabel() {
361            return mChooserTarget.getTitle();
362        }
363
364        @Override
365        public CharSequence getExtendedInfo() {
366            return mSourceInfo.getExtendedInfo();
367        }
368
369        @Override
370        public Drawable getDisplayIcon() {
371            return mDisplayIcon;
372        }
373    }
374
375    public class ChooserListAdapter extends ResolveListAdapter {
376        private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
377
378        public ChooserListAdapter(Context context, Intent[] initialIntents, List<ResolveInfo> rList,
379                int launchedFromUid, boolean filterLastUsed) {
380            super(context, initialIntents, rList, launchedFromUid, filterLastUsed);
381        }
382
383        @Override
384        public boolean showsExtendedInfo(TargetInfo info) {
385            // Reserve space to show extended info if any one of the items in the adapter has
386            // extended info. This keeps grid item sizes uniform.
387            return hasExtendedInfo();
388        }
389
390        @Override
391        public View createView(ViewGroup parent) {
392            return mInflater.inflate(
393                    com.android.internal.R.layout.resolve_grid_item, parent, false);
394        }
395
396        @Override
397        public void onListRebuilt() {
398            if (mServiceTargets != null) {
399                pruneServiceTargets();
400            }
401        }
402
403        @Override
404        public boolean shouldGetResolvedFilter() {
405            return true;
406        }
407
408        @Override
409        public int getCount() {
410            int count = super.getCount();
411            if (mServiceTargets != null) {
412                count += mServiceTargets.size();
413            }
414            return count;
415        }
416
417        @Override
418        public TargetInfo getItem(int position) {
419            int offset = 0;
420            if (mServiceTargets != null) {
421                final int serviceTargetCount = mServiceTargets.size();
422                if (position < serviceTargetCount) {
423                    return mServiceTargets.get(position);
424                }
425                offset += serviceTargetCount;
426            }
427            return super.getItem(position - offset);
428        }
429
430        public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets) {
431            if (DEBUG) Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
432                    + " targets");
433            for (int i = 0, N = targets.size(); i < N; i++) {
434                mServiceTargets.add(new ChooserTargetInfo(origTarget, targets.get(i)));
435            }
436
437            // TODO: Maintain sort by ranking scores.
438
439            notifyDataSetChanged();
440        }
441
442        private void pruneServiceTargets() {
443            if (DEBUG) Log.d(TAG, "pruneServiceTargets");
444            for (int i = mServiceTargets.size() - 1; i >= 0; i--) {
445                final ChooserTargetInfo cti = mServiceTargets.get(i);
446                if (!hasResolvedTarget(cti.getResolveInfo())) {
447                    if (DEBUG) Log.d(TAG, " => " + i + " " + cti);
448                    mServiceTargets.remove(i);
449                }
450            }
451        }
452    }
453
454    class ChooserTargetServiceConnection implements ServiceConnection {
455        private final DisplayResolveInfo mOriginalTarget;
456
457        private final IChooserTargetResult mChooserTargetResult = new IChooserTargetResult.Stub() {
458            @Override
459            public void sendResult(List<ChooserTarget> targets) throws RemoteException {
460                final Message msg = Message.obtain();
461                msg.what = CHOOSER_TARGET_SERVICE_RESULT;
462                msg.obj = new ServiceResultInfo(mOriginalTarget, targets,
463                        ChooserTargetServiceConnection.this);
464                mTargetResultHandler.sendMessage(msg);
465            }
466        };
467
468        public ChooserTargetServiceConnection(DisplayResolveInfo dri) {
469            mOriginalTarget = dri;
470        }
471
472        @Override
473        public void onServiceConnected(ComponentName name, IBinder service) {
474            if (DEBUG) Log.d(TAG, "onServiceConnected: " + name);
475            final IChooserTargetService icts = IChooserTargetService.Stub.asInterface(service);
476            try {
477                icts.getChooserTargets(mOriginalTarget.getResolvedComponentName(),
478                        mOriginalTarget.getResolveInfo().filter, mChooserTargetResult);
479            } catch (RemoteException e) {
480                Log.e(TAG, "Querying ChooserTargetService " + name + " failed.", e);
481                unbindService(this);
482                mServiceConnections.remove(this);
483            }
484        }
485
486        @Override
487        public void onServiceDisconnected(ComponentName name) {
488            if (DEBUG) Log.d(TAG, "onServiceDisconnected: " + name);
489            unbindService(this);
490            mServiceConnections.remove(this);
491        }
492
493        @Override
494        public String toString() {
495            return mOriginalTarget.getResolveInfo().activityInfo.toString();
496        }
497    }
498
499    static class ServiceResultInfo {
500        public final DisplayResolveInfo originalTarget;
501        public final List<ChooserTarget> resultTargets;
502        public final ChooserTargetServiceConnection connection;
503
504        public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
505                ChooserTargetServiceConnection c) {
506            originalTarget = ot;
507            resultTargets = rt;
508            connection = c;
509        }
510    }
511}
512