| /* |
| * Copyright 2018 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.usage.UsageStatsManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.os.Handler; |
| import android.os.Looper; |
| import android.os.Message; |
| import android.os.UserHandle; |
| import android.util.Log; |
| |
| import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; |
| |
| import java.text.Collator; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.List; |
| |
| /** |
| * Used to sort resolved activities in {@link ResolverListController}. |
| * |
| * @hide |
| */ |
| public abstract class AbstractResolverComparator implements Comparator<ResolvedComponentInfo> { |
| |
| private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3; |
| private static final boolean DEBUG = false; |
| private static final String TAG = "AbstractResolverComp"; |
| |
| protected AfterCompute mAfterCompute; |
| protected final PackageManager mPm; |
| protected final UsageStatsManager mUsm; |
| protected String[] mAnnotations; |
| protected String mContentType; |
| |
| // True if the current share is a link. |
| private final boolean mHttp; |
| // can be null if mHttp == false or current user has no default browser package |
| private final String mDefaultBrowserPackageName; |
| |
| // message types |
| static final int RANKER_SERVICE_RESULT = 0; |
| static final int RANKER_RESULT_TIMEOUT = 1; |
| |
| // timeout for establishing connections with a ResolverRankerService, collecting features and |
| // predicting ranking scores. |
| private static final int WATCHDOG_TIMEOUT_MILLIS = 500; |
| |
| private final Comparator<ResolveInfo> mAzComparator; |
| |
| protected final Handler mHandler = new Handler(Looper.getMainLooper()) { |
| public void handleMessage(Message msg) { |
| switch (msg.what) { |
| case RANKER_SERVICE_RESULT: |
| if (DEBUG) { |
| Log.d(TAG, "RANKER_SERVICE_RESULT"); |
| } |
| if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) { |
| handleResultMessage(msg); |
| mHandler.removeMessages(RANKER_RESULT_TIMEOUT); |
| afterCompute(); |
| } |
| break; |
| |
| case RANKER_RESULT_TIMEOUT: |
| if (DEBUG) { |
| Log.d(TAG, "RANKER_RESULT_TIMEOUT; unbinding services"); |
| } |
| mHandler.removeMessages(RANKER_SERVICE_RESULT); |
| afterCompute(); |
| break; |
| |
| default: |
| super.handleMessage(msg); |
| } |
| } |
| }; |
| |
| public AbstractResolverComparator(Context context, Intent intent) { |
| String scheme = intent.getScheme(); |
| mHttp = "http".equals(scheme) || "https".equals(scheme); |
| mContentType = intent.getType(); |
| getContentAnnotations(intent); |
| mPm = context.getPackageManager(); |
| mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); |
| mDefaultBrowserPackageName = mHttp |
| ? mPm.getDefaultBrowserPackageNameAsUser(UserHandle.myUserId()) |
| : null; |
| mAzComparator = new AzInfoComparator(context); |
| } |
| |
| // get annotations of content from intent. |
| private void getContentAnnotations(Intent intent) { |
| ArrayList<String> annotations = intent.getStringArrayListExtra( |
| Intent.EXTRA_CONTENT_ANNOTATIONS); |
| if (annotations != null) { |
| int size = annotations.size(); |
| if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) { |
| size = NUM_OF_TOP_ANNOTATIONS_TO_USE; |
| } |
| mAnnotations = new String[size]; |
| for (int i = 0; i < size; i++) { |
| mAnnotations[i] = annotations.get(i); |
| } |
| } |
| } |
| |
| /** |
| * Callback to be called when {@link #compute(List)} finishes. This signals to stop waiting. |
| */ |
| interface AfterCompute { |
| |
| void afterCompute(); |
| } |
| |
| void setCallBack(AfterCompute afterCompute) { |
| mAfterCompute = afterCompute; |
| } |
| |
| protected final void afterCompute() { |
| final AfterCompute afterCompute = mAfterCompute; |
| if (afterCompute != null) { |
| afterCompute.afterCompute(); |
| } |
| } |
| |
| @Override |
| public final int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) { |
| final ResolveInfo lhs = lhsp.getResolveInfoAt(0); |
| final ResolveInfo rhs = rhsp.getResolveInfoAt(0); |
| |
| // We want to put the one targeted to another user at the end of the dialog. |
| if (lhs.targetUserId != UserHandle.USER_CURRENT) { |
| return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1; |
| } |
| if (rhs.targetUserId != UserHandle.USER_CURRENT) { |
| return -1; |
| } |
| |
| if (mHttp) { |
| // Special case: we want filters that match URI paths/schemes to be |
| // ordered before others. This is for the case when opening URIs, |
| // to make native apps go above browsers - except for 1 even more special case |
| // which is the default browser, as we want that to go above them all. |
| if (isDefaultBrowser(lhs)) { |
| return -1; |
| } |
| |
| if (isDefaultBrowser(rhs)) { |
| return 1; |
| } |
| final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match); |
| final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match); |
| if (lhsSpecific != rhsSpecific) { |
| return lhsSpecific ? -1 : 1; |
| } |
| } |
| |
| final boolean lPinned = lhsp.isPinned(); |
| final boolean rPinned = rhsp.isPinned(); |
| |
| // Pinned items always receive priority. |
| if (lPinned && !rPinned) { |
| return -1; |
| } else if (!lPinned && rPinned) { |
| return 1; |
| } else if (lPinned && rPinned) { |
| // If both items are pinned, resolve the tie alphabetically. |
| return mAzComparator.compare(lhsp.getResolveInfoAt(0), rhsp.getResolveInfoAt(0)); |
| } |
| |
| return compare(lhs, rhs); |
| } |
| |
| /** |
| * Delegated to when used as a {@link Comparator<ResolvedComponentInfo>} if there is not a |
| * special case. The {@link ResolveInfo ResolveInfos} are the first {@link ResolveInfo} in |
| * {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link |
| * #compare(ResolvedComponentInfo, ResolvedComponentInfo)} |
| */ |
| abstract int compare(ResolveInfo lhs, ResolveInfo rhs); |
| |
| /** |
| * Computes features for each target. This will be called before calls to {@link |
| * #getScore(ComponentName)} or {@link #compare(Object, Object)}, in order to prepare the |
| * comparator for those calls. Note that {@link #getScore(ComponentName)} uses {@link |
| * ComponentName}, so the implementation will have to be prepared to identify a {@link |
| * ResolvedComponentInfo} by {@link ComponentName}. {@link #beforeCompute()} will be called |
| * before doing any computing. |
| */ |
| final void compute(List<ResolvedComponentInfo> targets) { |
| beforeCompute(); |
| doCompute(targets); |
| } |
| |
| /** Implementation of compute called after {@link #beforeCompute()}. */ |
| abstract void doCompute(List<ResolvedComponentInfo> targets); |
| |
| /** |
| * Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo} |
| * when {@link #compute(List)} was called before this. |
| */ |
| abstract float getScore(ComponentName name); |
| |
| /** Handles result message sent to mHandler. */ |
| abstract void handleResultMessage(Message message); |
| |
| /** |
| * Reports to UsageStats what was chosen. |
| */ |
| final void updateChooserCounts(String packageName, int userId, String action) { |
| if (mUsm != null) { |
| mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action); |
| } |
| } |
| |
| /** |
| * Updates the model used to rank the componentNames. |
| * |
| * <p>Default implementation does nothing, as we could have simple model that does not train |
| * online. |
| * |
| * @param componentName the component that the user clicked |
| */ |
| void updateModel(ComponentName componentName) { |
| } |
| |
| /** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */ |
| void beforeCompute() { |
| if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + WATCHDOG_TIMEOUT_MILLIS + "ms"); |
| if (mHandler == null) { |
| Log.d(TAG, "Error: Handler is Null; Needs to be initialized."); |
| return; |
| } |
| mHandler.sendEmptyMessageDelayed(RANKER_RESULT_TIMEOUT, WATCHDOG_TIMEOUT_MILLIS); |
| } |
| |
| /** |
| * Called when the {@link ResolverActivity} is destroyed. This calls {@link #afterCompute()}. If |
| * this call needs to happen at a different time during destroy, the method should be |
| * overridden. |
| */ |
| void destroy() { |
| mHandler.removeMessages(RANKER_SERVICE_RESULT); |
| mHandler.removeMessages(RANKER_RESULT_TIMEOUT); |
| afterCompute(); |
| } |
| |
| private boolean isDefaultBrowser(ResolveInfo ri) { |
| // It makes sense to prefer the default browser |
| // only if the targeted user is the current user |
| if (ri.targetUserId != UserHandle.USER_CURRENT) { |
| return false; |
| } |
| |
| if (ri.activityInfo.packageName != null |
| && ri.activityInfo.packageName.equals(mDefaultBrowserPackageName)) { |
| return true; |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Sort intents alphabetically based on package name. |
| */ |
| class AzInfoComparator implements Comparator<ResolveInfo> { |
| Collator mCollator; |
| AzInfoComparator(Context context) { |
| mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); |
| } |
| |
| @Override |
| public int compare(ResolveInfo lhsp, ResolveInfo rhsp) { |
| if (lhsp == null) { |
| return -1; |
| } else if (rhsp == null) { |
| return 1; |
| } |
| return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName); |
| } |
| } |
| |
| } |