/* * Copyright (C) 2016 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.annotation.WorkerThread; import android.app.ActivityManager; import android.app.AppGlobals; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.RemoteException; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import java.lang.InterruptedException; import java.util.ArrayList; import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.List; /** * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of * resolvers. */ public class ResolverListController { private final Context mContext; private final PackageManager mpm; private final int mLaunchedFromUid; // Needed for sorting resolvers. private final Intent mTargetIntent; private final String mReferrerPackage; private static final String TAG = "ResolverListController"; private static final boolean DEBUG = false; Object mLock = new Object(); @GuardedBy("mLock") private ResolverComparator mResolverComparator; private boolean isComputed = false; public ResolverListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackage, int launchedFromUid) { mContext = context; mpm = pm; mLaunchedFromUid = launchedFromUid; mTargetIntent = targetIntent; mReferrerPackage = referrerPackage; synchronized (mLock) { mResolverComparator = new ResolverComparator(mContext, mTargetIntent, mReferrerPackage, null); } } @VisibleForTesting public ResolveInfo getLastChosen() throws RemoteException { return AppGlobals.getPackageManager().getLastChosenActivity( mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()), PackageManager.MATCH_DEFAULT_ONLY); } @VisibleForTesting public void setLastChosen(Intent intent, IntentFilter filter, int match) throws RemoteException { AppGlobals.getPackageManager().setLastChosenActivity(intent, intent.resolveType(mContext.getContentResolver()), PackageManager.MATCH_DEFAULT_ONLY, filter, match, intent.getComponent()); } @VisibleForTesting public List getResolversForIntent( boolean shouldGetResolvedFilter, boolean shouldGetActivityMetadata, List intents) { List resolvedComponents = null; for (int i = 0, N = intents.size(); i < N; i++) { final Intent intent = intents.get(i); final List infos = mpm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0) | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0) | PackageManager.MATCH_INSTANT); // Remove any activities that are not exported. int totalSize = infos.size(); for (int j = totalSize - 1; j >= 0 ; j--) { ResolveInfo info = infos.get(j); if (info.activityInfo != null && !info.activityInfo.exported) { infos.remove(j); } } if (infos != null) { if (resolvedComponents == null) { resolvedComponents = new ArrayList<>(); } addResolveListDedupe(resolvedComponents, intent, infos); } } return resolvedComponents; } @VisibleForTesting public void addResolveListDedupe(List into, Intent intent, List from) { final int fromCount = from.size(); final int intoCount = into.size(); for (int i = 0; i < fromCount; i++) { final ResolveInfo newInfo = from.get(i); boolean found = false; // Only loop to the end of into as it was before we started; no dupes in from. for (int j = 0; j < intoCount; j++) { final ResolverActivity.ResolvedComponentInfo rci = into.get(j); if (isSameResolvedComponent(newInfo, rci)) { found = true; rci.add(intent, newInfo); break; } } if (!found) { final ComponentName name = new ComponentName( newInfo.activityInfo.packageName, newInfo.activityInfo.name); final ResolverActivity.ResolvedComponentInfo rci = new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo); rci.setPinned(isComponentPinned(name)); into.add(rci); } } } // Filter out any activities that the launched uid does not have permission for. // // Also filter out those that are suspended because they couldn't be started. We don't do this // when we have an explicit list of resolved activities, because that only happens when // we are being subclassed, so we can safely launch whatever they gave us. // // To preserve the inputList, optionally will return the original list if any modification has // been made. @VisibleForTesting public ArrayList filterIneligibleActivities( List inputList, boolean returnCopyOfOriginalListIfModified) { ArrayList listToReturn = null; for (int i = inputList.size()-1; i >= 0; i--) { ActivityInfo ai = inputList.get(i) .getResolveInfoAt(0).activityInfo; int granted = ActivityManager.checkComponentPermission( ai.permission, mLaunchedFromUid, ai.applicationInfo.uid, ai.exported); boolean suspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; if (granted != PackageManager.PERMISSION_GRANTED || suspended || isComponentFiltered(ai.getComponentName())) { // Access not allowed! We're about to filter an item, // so modify the unfiltered version if it hasn't already been modified. if (returnCopyOfOriginalListIfModified && listToReturn == null) { listToReturn = new ArrayList<>(inputList); } inputList.remove(i); } } return listToReturn; } // Filter out any low priority items. // // To preserve the inputList, optionally will return the original list if any modification has // been made. @VisibleForTesting public ArrayList filterLowPriority( List inputList, boolean returnCopyOfOriginalListIfModified) { ArrayList listToReturn = null; // Only display the first matches that are either of equal // priority or have asked to be default options. ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0); ResolveInfo r0 = rci0.getResolveInfoAt(0); int N = inputList.size(); for (int i = 1; i < N; i++) { ResolveInfo ri = inputList.get(i).getResolveInfoAt(0); if (DEBUG) Log.v( TAG, r0.activityInfo.name + "=" + r0.priority + "/" + r0.isDefault + " vs " + ri.activityInfo.name + "=" + ri.priority + "/" + ri.isDefault); if (r0.priority != ri.priority || r0.isDefault != ri.isDefault) { while (i < N) { if (returnCopyOfOriginalListIfModified && listToReturn == null) { listToReturn = new ArrayList<>(inputList); } inputList.remove(i); N--; } } } return listToReturn; } private class ComputeCallback implements ResolverComparator.AfterCompute { private CountDownLatch mFinishComputeSignal; public ComputeCallback(CountDownLatch finishComputeSignal) { mFinishComputeSignal = finishComputeSignal; } public void afterCompute () { mFinishComputeSignal.countDown(); } } @VisibleForTesting @WorkerThread public void sort(List inputList) { synchronized (mLock) { if (mResolverComparator == null) { Log.d(TAG, "Comparator has already been destroyed; skipped."); return; } final CountDownLatch finishComputeSignal = new CountDownLatch(1); ComputeCallback callback = new ComputeCallback(finishComputeSignal); mResolverComparator.setCallBack(callback); try { long beforeRank = System.currentTimeMillis(); if (!isComputed) { mResolverComparator.compute(inputList); finishComputeSignal.await(); isComputed = true; } Collections.sort(inputList, mResolverComparator); long afterRank = System.currentTimeMillis(); if (DEBUG) { Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank)); } } catch (InterruptedException e) { Log.e(TAG, "Compute & Sort was interrupted: " + e); } } } private static boolean isSameResolvedComponent(ResolveInfo a, ResolverActivity.ResolvedComponentInfo b) { final ActivityInfo ai = a.activityInfo; return ai.packageName.equals(b.name.getPackageName()) && ai.name.equals(b.name.getClassName()); } boolean isComponentPinned(ComponentName name) { return false; } boolean isComponentFiltered(ComponentName componentName) { return false; } @VisibleForTesting public float getScore(ResolverActivity.DisplayResolveInfo target) { synchronized (mLock) { if (mResolverComparator == null) { return 0.0f; } return mResolverComparator.getScore(target.getResolvedComponentName()); } } public void updateModel(ComponentName componentName) { synchronized (mLock) { if (mResolverComparator != null) { mResolverComparator.updateModel(componentName); } } } public void updateChooserCounts(String packageName, int userId, String action) { synchronized (mLock) { if (mResolverComparator != null) { mResolverComparator.updateChooserCounts(packageName, userId, action); } } } public void destroy() { synchronized (mLock) { if (mResolverComparator != null) { mResolverComparator.destroy(); } mResolverComparator = null; } } }