ResolverComparator.java revision 78c6efcdf31f79cd0ece1bf642f913e3991ad384
1/*
2 * Copyright (C) 2015 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
17
18package com.android.internal.app;
19
20import android.app.usage.UsageStats;
21import android.app.usage.UsageStatsManager;
22import android.content.ComponentName;
23import android.content.Context;
24import android.content.Intent;
25import android.content.IntentFilter;
26import android.content.pm.ApplicationInfo;
27import android.content.pm.ComponentInfo;
28import android.content.pm.PackageManager;
29import android.content.pm.PackageManager.NameNotFoundException;
30import android.content.pm.ResolveInfo;
31import android.content.SharedPreferences;
32import android.content.ServiceConnection;
33import android.metrics.LogMaker;
34import android.os.Environment;
35import android.os.Handler;
36import android.os.IBinder;
37import android.os.Looper;
38import android.os.Message;
39import android.os.RemoteException;
40import android.os.storage.StorageManager;
41import android.os.UserHandle;
42import android.service.resolver.IResolverRankerService;
43import android.service.resolver.IResolverRankerResult;
44import android.service.resolver.ResolverRankerService;
45import android.service.resolver.ResolverTarget;
46import android.text.TextUtils;
47import android.util.ArrayMap;
48import android.util.Log;
49import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
50import com.android.internal.logging.MetricsLogger;
51import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
52
53import java.io.File;
54import java.lang.InterruptedException;
55import java.text.Collator;
56import java.util.ArrayList;
57import java.util.Comparator;
58import java.util.concurrent.CountDownLatch;
59import java.util.concurrent.TimeUnit;
60import java.util.LinkedHashMap;
61import java.util.List;
62import java.util.Map;
63
64/**
65 * Ranks and compares packages based on usage stats.
66 */
67class ResolverComparator implements Comparator<ResolvedComponentInfo> {
68    private static final String TAG = "ResolverComparator";
69
70    private static final boolean DEBUG = false;
71
72    private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3;
73
74    // One week
75    private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7;
76
77    private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
78
79    private static final float RECENCY_MULTIPLIER = 2.f;
80
81    // message types
82    private static final int RESOLVER_RANKER_SERVICE_RESULT = 0;
83    private static final int RESOLVER_RANKER_RESULT_TIMEOUT = 1;
84
85    // timeout for establishing connections with a ResolverRankerService.
86    private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200;
87    // timeout for establishing connections with a ResolverRankerService, collecting features and
88    // predicting ranking scores.
89    private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
90
91    private final Collator mCollator;
92    private final boolean mHttp;
93    private final PackageManager mPm;
94    private final UsageStatsManager mUsm;
95    private final Map<String, UsageStats> mStats;
96    private final long mCurrentTime;
97    private final long mSinceTime;
98    private final LinkedHashMap<ComponentName, ResolverTarget> mTargetsDict = new LinkedHashMap<>();
99    private final String mReferrerPackage;
100    private final Object mLock = new Object();
101    private ArrayList<ResolverTarget> mTargets;
102    private String mContentType;
103    private String[] mAnnotations;
104    private String mAction;
105    private ComponentName mResolvedRankerName;
106    private ComponentName mRankerServiceName;
107    private IResolverRankerService mRanker;
108    private ResolverRankerServiceConnection mConnection;
109    private AfterCompute mAfterCompute;
110    private Context mContext;
111    private CountDownLatch mConnectSignal;
112
113    private final Handler mHandler = new Handler(Looper.getMainLooper()) {
114        public void handleMessage(Message msg) {
115            switch (msg.what) {
116                case RESOLVER_RANKER_SERVICE_RESULT:
117                    if (DEBUG) {
118                        Log.d(TAG, "RESOLVER_RANKER_SERVICE_RESULT");
119                    }
120                    if (mHandler.hasMessages(RESOLVER_RANKER_RESULT_TIMEOUT)) {
121                        if (msg.obj != null) {
122                            final List<ResolverTarget> receivedTargets =
123                                    (List<ResolverTarget>) msg.obj;
124                            if (receivedTargets != null && mTargets != null
125                                    && receivedTargets.size() == mTargets.size()) {
126                                final int size = mTargets.size();
127                                boolean isUpdated = false;
128                                for (int i = 0; i < size; ++i) {
129                                    final float predictedProb =
130                                            receivedTargets.get(i).getSelectProbability();
131                                    if (predictedProb != mTargets.get(i).getSelectProbability()) {
132                                        mTargets.get(i).setSelectProbability(predictedProb);
133                                        isUpdated = true;
134                                    }
135                                }
136                                if (isUpdated) {
137                                    mRankerServiceName = mResolvedRankerName;
138                                }
139                            } else {
140                                Log.e(TAG, "Sizes of sent and received ResolverTargets diff.");
141                            }
142                        } else {
143                            Log.e(TAG, "Receiving null prediction results.");
144                        }
145                        mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
146                        mAfterCompute.afterCompute();
147                    }
148                    break;
149
150                case RESOLVER_RANKER_RESULT_TIMEOUT:
151                    if (DEBUG) {
152                        Log.d(TAG, "RESOLVER_RANKER_RESULT_TIMEOUT; unbinding services");
153                    }
154                    mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
155                    mAfterCompute.afterCompute();
156                    break;
157
158                default:
159                    super.handleMessage(msg);
160            }
161        }
162    };
163
164    public interface AfterCompute {
165        public void afterCompute ();
166    }
167
168    public ResolverComparator(Context context, Intent intent, String referrerPackage,
169                              AfterCompute afterCompute) {
170        mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
171        String scheme = intent.getScheme();
172        mHttp = "http".equals(scheme) || "https".equals(scheme);
173        mReferrerPackage = referrerPackage;
174        mAfterCompute = afterCompute;
175        mContext = context;
176
177        mPm = context.getPackageManager();
178        mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
179
180        mCurrentTime = System.currentTimeMillis();
181        mSinceTime = mCurrentTime - USAGE_STATS_PERIOD;
182        mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime);
183        mContentType = intent.getType();
184        getContentAnnotations(intent);
185        mAction = intent.getAction();
186        mRankerServiceName = new ComponentName(mContext, this.getClass());
187    }
188
189    // get annotations of content from intent.
190    public void getContentAnnotations(Intent intent) {
191        ArrayList<String> annotations = intent.getStringArrayListExtra(
192                Intent.EXTRA_CONTENT_ANNOTATIONS);
193        if (annotations != null) {
194            int size = annotations.size();
195            if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) {
196                size = NUM_OF_TOP_ANNOTATIONS_TO_USE;
197            }
198            mAnnotations = new String[size];
199            for (int i = 0; i < size; i++) {
200                mAnnotations[i] = annotations.get(i);
201            }
202        }
203    }
204
205    public void setCallBack(AfterCompute afterCompute) {
206        mAfterCompute = afterCompute;
207    }
208
209    // compute features for each target according to usage stats of targets.
210    public void compute(List<ResolvedComponentInfo> targets) {
211        reset();
212
213        final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD;
214
215        float mostRecencyScore = 1.0f;
216        float mostTimeSpentScore = 1.0f;
217        float mostLaunchScore = 1.0f;
218        float mostChooserScore = 1.0f;
219
220        for (ResolvedComponentInfo target : targets) {
221            final ResolverTarget resolverTarget = new ResolverTarget();
222            mTargetsDict.put(target.name, resolverTarget);
223            final UsageStats pkStats = mStats.get(target.name.getPackageName());
224            if (pkStats != null) {
225                // Only count recency for apps that weren't the caller
226                // since the caller is always the most recent.
227                // Persistent processes muck this up, so omit them too.
228                if (!target.name.getPackageName().equals(mReferrerPackage)
229                        && !isPersistentProcess(target)) {
230                    final float recencyScore =
231                            (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0);
232                    resolverTarget.setRecencyScore(recencyScore);
233                    if (recencyScore > mostRecencyScore) {
234                        mostRecencyScore = recencyScore;
235                    }
236                }
237                final float timeSpentScore = (float) pkStats.getTotalTimeInForeground();
238                resolverTarget.setTimeSpentScore(timeSpentScore);
239                if (timeSpentScore > mostTimeSpentScore) {
240                    mostTimeSpentScore = timeSpentScore;
241                }
242                final float launchScore = (float) pkStats.mLaunchCount;
243                resolverTarget.setLaunchScore(launchScore);
244                if (launchScore > mostLaunchScore) {
245                    mostLaunchScore = launchScore;
246                }
247
248                float chooserScore = 0.0f;
249                if (pkStats.mChooserCounts != null && mAction != null
250                        && pkStats.mChooserCounts.get(mAction) != null) {
251                    chooserScore = (float) pkStats.mChooserCounts.get(mAction)
252                            .getOrDefault(mContentType, 0);
253                    if (mAnnotations != null) {
254                        final int size = mAnnotations.length;
255                        for (int i = 0; i < size; i++) {
256                            chooserScore += (float) pkStats.mChooserCounts.get(mAction)
257                                    .getOrDefault(mAnnotations[i], 0);
258                        }
259                    }
260                }
261                if (DEBUG) {
262                    if (mAction == null) {
263                        Log.d(TAG, "Action type is null");
264                    } else {
265                        Log.d(TAG, "Chooser Count of " + mAction + ":" +
266                                target.name.getPackageName() + " is " +
267                                Float.toString(chooserScore));
268                    }
269                }
270                resolverTarget.setChooserScore(chooserScore);
271                if (chooserScore > mostChooserScore) {
272                    mostChooserScore = chooserScore;
273                }
274            }
275        }
276
277        if (DEBUG) {
278            Log.d(TAG, "compute - mostRecencyScore: " + mostRecencyScore
279                    + " mostTimeSpentScore: " + mostTimeSpentScore
280                    + " mostLaunchScore: " + mostLaunchScore
281                    + " mostChooserScore: " + mostChooserScore);
282        }
283
284        mTargets = new ArrayList<>(mTargetsDict.values());
285        for (ResolverTarget target : mTargets) {
286            final float recency = target.getRecencyScore() / mostRecencyScore;
287            setFeatures(target, recency * recency * RECENCY_MULTIPLIER,
288                    target.getLaunchScore() / mostLaunchScore,
289                    target.getTimeSpentScore() / mostTimeSpentScore,
290                    target.getChooserScore() / mostChooserScore);
291            addDefaultSelectProbability(target);
292            if (DEBUG) {
293                Log.d(TAG, "Scores: " + target);
294            }
295        }
296        predictSelectProbabilities(mTargets);
297    }
298
299    @Override
300    public int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
301        final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
302        final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
303
304        // We want to put the one targeted to another user at the end of the dialog.
305        if (lhs.targetUserId != UserHandle.USER_CURRENT) {
306            return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
307        }
308        if (rhs.targetUserId != UserHandle.USER_CURRENT) {
309            return -1;
310        }
311
312        if (mHttp) {
313            // Special case: we want filters that match URI paths/schemes to be
314            // ordered before others.  This is for the case when opening URIs,
315            // to make native apps go above browsers.
316            final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
317            final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
318            if (lhsSpecific != rhsSpecific) {
319                return lhsSpecific ? -1 : 1;
320            }
321        }
322
323        final boolean lPinned = lhsp.isPinned();
324        final boolean rPinned = rhsp.isPinned();
325
326        if (lPinned && !rPinned) {
327            return -1;
328        } else if (!lPinned && rPinned) {
329            return 1;
330        }
331
332        // Pinned items stay stable within a normal lexical sort and ignore scoring.
333        if (!lPinned && !rPinned) {
334            if (mStats != null) {
335                final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
336                        lhs.activityInfo.packageName, lhs.activityInfo.name));
337                final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
338                        rhs.activityInfo.packageName, rhs.activityInfo.name));
339
340                final int selectProbabilityDiff = Float.compare(
341                        rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
342
343                if (selectProbabilityDiff != 0) {
344                    return selectProbabilityDiff > 0 ? 1 : -1;
345                }
346            }
347        }
348
349        CharSequence  sa = lhs.loadLabel(mPm);
350        if (sa == null) sa = lhs.activityInfo.name;
351        CharSequence  sb = rhs.loadLabel(mPm);
352        if (sb == null) sb = rhs.activityInfo.name;
353
354        return mCollator.compare(sa.toString().trim(), sb.toString().trim());
355    }
356
357    public float getScore(ComponentName name) {
358        final ResolverTarget target = mTargetsDict.get(name);
359        if (target != null) {
360            return target.getSelectProbability();
361        }
362        return 0;
363    }
364
365    public void updateChooserCounts(String packageName, int userId, String action) {
366        if (mUsm != null) {
367            mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
368        }
369    }
370
371    // update ranking model when the connection to it is valid.
372    public void updateModel(ComponentName componentName) {
373        synchronized (mLock) {
374            if (mRanker != null) {
375                try {
376                    int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
377                            .indexOf(componentName);
378                    if (selectedPos >= 0 && mTargets != null) {
379                        final float selectedProbability = getScore(componentName);
380                        int order = 0;
381                        for (ResolverTarget target : mTargets) {
382                            if (target.getSelectProbability() > selectedProbability) {
383                                order++;
384                            }
385                        }
386                        logMetrics(order);
387                        mRanker.train(mTargets, selectedPos);
388                    } else {
389                        if (DEBUG) {
390                            Log.d(TAG, "Selected a unknown component: " + componentName);
391                        }
392                    }
393                } catch (RemoteException e) {
394                    Log.e(TAG, "Error in Train: " + e);
395                }
396            } else {
397                if (DEBUG) {
398                    Log.d(TAG, "Ranker is null; skip updateModel.");
399                }
400            }
401        }
402    }
403
404    // unbind the service and clear unhandled messges.
405    public void destroy() {
406        mHandler.removeMessages(RESOLVER_RANKER_SERVICE_RESULT);
407        mHandler.removeMessages(RESOLVER_RANKER_RESULT_TIMEOUT);
408        if (mConnection != null) {
409            mContext.unbindService(mConnection);
410            mConnection.destroy();
411        }
412        if (DEBUG) {
413            Log.d(TAG, "Unbinded Resolver Ranker.");
414        }
415    }
416
417    // records metrics for evaluation.
418    private void logMetrics(int selectedPos) {
419        if (mRankerServiceName != null) {
420            MetricsLogger metricsLogger = new MetricsLogger();
421            LogMaker log = new LogMaker(MetricsEvent.ACTION_TARGET_SELECTED);
422            log.setComponentName(mRankerServiceName);
423            int isCategoryUsed = (mAnnotations == null) ? 0 : 1;
424            log.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, isCategoryUsed);
425            log.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, selectedPos);
426            metricsLogger.write(log);
427        }
428    }
429
430    // connect to a ranking service.
431    private void initRanker(Context context) {
432        synchronized (mLock) {
433            if (mConnection != null && mRanker != null) {
434                if (DEBUG) {
435                    Log.d(TAG, "Ranker still exists; reusing the existing one.");
436                }
437                return;
438            }
439        }
440        Intent intent = resolveRankerService();
441        if (intent == null) {
442            return;
443        }
444        mConnectSignal = new CountDownLatch(1);
445        mConnection = new ResolverRankerServiceConnection(mConnectSignal);
446        context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
447    }
448
449    // resolve the service for ranking.
450    private Intent resolveRankerService() {
451        Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE);
452        final List<ResolveInfo> resolveInfos = mPm.queryIntentServices(intent, 0);
453        for (ResolveInfo resolveInfo : resolveInfos) {
454            if (resolveInfo == null || resolveInfo.serviceInfo == null
455                    || resolveInfo.serviceInfo.applicationInfo == null) {
456                if (DEBUG) {
457                    Log.d(TAG, "Failed to retrieve a ranker: " + resolveInfo);
458                }
459                continue;
460            }
461            ComponentName componentName = new ComponentName(
462                    resolveInfo.serviceInfo.applicationInfo.packageName,
463                    resolveInfo.serviceInfo.name);
464            try {
465                final String perm = mPm.getServiceInfo(componentName, 0).permission;
466                if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) {
467                    Log.w(TAG, "ResolverRankerService " + componentName + " does not require"
468                            + " permission " + ResolverRankerService.BIND_PERMISSION
469                            + " - this service will not be queried for ResolverComparator."
470                            + " add android:permission=\""
471                            + ResolverRankerService.BIND_PERMISSION + "\""
472                            + " to the <service> tag for " + componentName
473                            + " in the manifest.");
474                    continue;
475                }
476                if (PackageManager.PERMISSION_GRANTED != mPm.checkPermission(
477                        ResolverRankerService.HOLD_PERMISSION,
478                        resolveInfo.serviceInfo.packageName)) {
479                    Log.w(TAG, "ResolverRankerService " + componentName + " does not hold"
480                            + " permission " + ResolverRankerService.HOLD_PERMISSION
481                            + " - this service will not be queried for ResolverComparator.");
482                    continue;
483                }
484            } catch (NameNotFoundException e) {
485                Log.e(TAG, "Could not look up service " + componentName
486                        + "; component name not found");
487                continue;
488            }
489            if (DEBUG) {
490                Log.d(TAG, "Succeeded to retrieve a ranker: " + componentName);
491            }
492            mResolvedRankerName = componentName;
493            intent.setComponent(componentName);
494            return intent;
495        }
496        return null;
497    }
498
499    // set a watchdog, to avoid waiting for ranking service for too long.
500    private void startWatchDog(int timeOutLimit) {
501        if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + timeOutLimit + "ms");
502        if (mHandler == null) {
503            Log.d(TAG, "Error: Handler is Null; Needs to be initialized.");
504        }
505        mHandler.sendEmptyMessageDelayed(RESOLVER_RANKER_RESULT_TIMEOUT, timeOutLimit);
506    }
507
508    private class ResolverRankerServiceConnection implements ServiceConnection {
509        private final CountDownLatch mConnectSignal;
510
511        public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
512            mConnectSignal = connectSignal;
513        }
514
515        public final IResolverRankerResult resolverRankerResult =
516                new IResolverRankerResult.Stub() {
517            @Override
518            public void sendResult(List<ResolverTarget> targets) throws RemoteException {
519                if (DEBUG) {
520                    Log.d(TAG, "Sending Result back to Resolver: " + targets);
521                }
522                synchronized (mLock) {
523                    final Message msg = Message.obtain();
524                    msg.what = RESOLVER_RANKER_SERVICE_RESULT;
525                    msg.obj = targets;
526                    mHandler.sendMessage(msg);
527                }
528            }
529        };
530
531        @Override
532        public void onServiceConnected(ComponentName name, IBinder service) {
533            if (DEBUG) {
534                Log.d(TAG, "onServiceConnected: " + name);
535            }
536            synchronized (mLock) {
537                mRanker = IResolverRankerService.Stub.asInterface(service);
538                mConnectSignal.countDown();
539            }
540        }
541
542        @Override
543        public void onServiceDisconnected(ComponentName name) {
544            if (DEBUG) {
545                Log.d(TAG, "onServiceDisconnected: " + name);
546            }
547            synchronized (mLock) {
548                destroy();
549            }
550        }
551
552        public void destroy() {
553            synchronized (mLock) {
554                mRanker = null;
555            }
556        }
557    }
558
559    private void reset() {
560        mTargetsDict.clear();
561        mTargets = null;
562        mRankerServiceName = new ComponentName(mContext, this.getClass());
563        mResolvedRankerName = null;
564        startWatchDog(WATCHDOG_TIMEOUT_MILLIS);
565        initRanker(mContext);
566    }
567
568    // predict select probabilities if ranking service is valid.
569    private void predictSelectProbabilities(List<ResolverTarget> targets) {
570        if (mConnection == null) {
571            if (DEBUG) {
572                Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction");
573            }
574            return;
575        } else {
576            try {
577                mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
578                synchronized (mLock) {
579                    if (mRanker != null) {
580                        mRanker.predict(targets, mConnection.resolverRankerResult);
581                        return;
582                    } else {
583                        if (DEBUG) {
584                            Log.d(TAG, "Ranker has not been initialized; skip predict.");
585                        }
586                    }
587                }
588            } catch (InterruptedException e) {
589                Log.e(TAG, "Error in Wait for Service Connection.");
590            } catch (RemoteException e) {
591                Log.e(TAG, "Error in Predict: " + e);
592            }
593        }
594        if (mAfterCompute != null) {
595            mAfterCompute.afterCompute();
596        }
597    }
598
599    // adds select prob as the default values, according to a pre-trained Logistic Regression model.
600    private void addDefaultSelectProbability(ResolverTarget target) {
601        float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
602                0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
603        target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
604    }
605
606    // sets features for each target
607    private void setFeatures(ResolverTarget target, float recencyScore, float launchScore,
608                             float timeSpentScore, float chooserScore) {
609        target.setRecencyScore(recencyScore);
610        target.setLaunchScore(launchScore);
611        target.setTimeSpentScore(timeSpentScore);
612        target.setChooserScore(chooserScore);
613    }
614
615    static boolean isPersistentProcess(ResolvedComponentInfo rci) {
616        if (rci != null && rci.getCount() > 0) {
617            return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
618                    ApplicationInfo.FLAG_PERSISTENT) != 0;
619        }
620        return false;
621    }
622}
623