| /* |
| * Copyright (C) 2019 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 static android.content.Context.ACTIVITY_SERVICE; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.ActivityManager; |
| import android.content.ComponentName; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.PermissionChecker; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.ApplicationInfo; |
| import android.content.pm.LabeledIntent; |
| import android.content.pm.PackageManager; |
| import android.content.pm.ResolveInfo; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.ColorMatrix; |
| import android.graphics.ColorMatrixColorFilter; |
| import android.graphics.drawable.BitmapDrawable; |
| import android.graphics.drawable.Drawable; |
| import android.os.AsyncTask; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.AbsListView; |
| import android.widget.BaseAdapter; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import com.android.internal.R; |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.app.ResolverActivity.ResolvedComponentInfo; |
| import com.android.internal.app.chooser.DisplayResolveInfo; |
| import com.android.internal.app.chooser.TargetInfo; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| public class ResolverListAdapter extends BaseAdapter { |
| private static final String TAG = "ResolverListAdapter"; |
| |
| private final List<Intent> mIntents; |
| private final Intent[] mInitialIntents; |
| private final List<ResolveInfo> mBaseResolveList; |
| private final PackageManager mPm; |
| protected final Context mContext; |
| private final ColorMatrixColorFilter mSuspendedMatrixColorFilter; |
| private final boolean mUseLayoutForBrowsables; |
| private final int mIconDpi; |
| protected ResolveInfo mLastChosen; |
| private DisplayResolveInfo mOtherProfile; |
| ResolverListController mResolverListController; |
| private int mPlaceholderCount; |
| private boolean mAllTargetsAreBrowsers = false; |
| |
| protected final LayoutInflater mInflater; |
| |
| // This one is the list that the Adapter will actually present. |
| List<DisplayResolveInfo> mDisplayList; |
| List<ResolvedComponentInfo> mUnfilteredResolveList; |
| |
| private int mLastChosenPosition = -1; |
| private boolean mFilterLastUsed; |
| private final ResolverListCommunicator mResolverListCommunicator; |
| private Runnable mPostListReadyRunnable; |
| private final boolean mIsAudioCaptureDevice; |
| |
| public ResolverListAdapter(Context context, List<Intent> payloadIntents, |
| Intent[] initialIntents, List<ResolveInfo> rList, |
| boolean filterLastUsed, |
| ResolverListController resolverListController, |
| boolean useLayoutForBrowsables, |
| ResolverListCommunicator resolverListCommunicator, |
| boolean isAudioCaptureDevice) { |
| mContext = context; |
| mIntents = payloadIntents; |
| mInitialIntents = initialIntents; |
| mBaseResolveList = rList; |
| mInflater = LayoutInflater.from(context); |
| mPm = context.getPackageManager(); |
| mDisplayList = new ArrayList<>(); |
| mFilterLastUsed = filterLastUsed; |
| mResolverListController = resolverListController; |
| mSuspendedMatrixColorFilter = createSuspendedColorMatrix(); |
| mUseLayoutForBrowsables = useLayoutForBrowsables; |
| mResolverListCommunicator = resolverListCommunicator; |
| mIsAudioCaptureDevice = isAudioCaptureDevice; |
| final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); |
| mIconDpi = am.getLauncherLargeIconDensity(); |
| } |
| |
| public void handlePackagesChanged() { |
| rebuildList(); |
| mResolverListCommunicator.onHandlePackagesChanged(); |
| } |
| |
| public void setPlaceholderCount(int count) { |
| mPlaceholderCount = count; |
| } |
| |
| public int getPlaceholderCount() { |
| return mPlaceholderCount; |
| } |
| |
| @Nullable |
| public DisplayResolveInfo getFilteredItem() { |
| if (mFilterLastUsed && mLastChosenPosition >= 0) { |
| // Not using getItem since it offsets to dodge this position for the list |
| return mDisplayList.get(mLastChosenPosition); |
| } |
| return null; |
| } |
| |
| public DisplayResolveInfo getOtherProfile() { |
| return mOtherProfile; |
| } |
| |
| public int getFilteredPosition() { |
| if (mFilterLastUsed && mLastChosenPosition >= 0) { |
| return mLastChosenPosition; |
| } |
| return AbsListView.INVALID_POSITION; |
| } |
| |
| public boolean hasFilteredItem() { |
| return mFilterLastUsed && mLastChosen != null; |
| } |
| |
| public float getScore(DisplayResolveInfo target) { |
| return mResolverListController.getScore(target); |
| } |
| |
| public void updateModel(ComponentName componentName) { |
| mResolverListController.updateModel(componentName); |
| } |
| |
| public void updateChooserCounts(String packageName, int userId, String action) { |
| mResolverListController.updateChooserCounts(packageName, userId, action); |
| } |
| |
| /** |
| * @return true if all items in the display list are defined as browsers by |
| * ResolveInfo.handleAllWebDataURI |
| */ |
| public boolean areAllTargetsBrowsers() { |
| return mAllTargetsAreBrowsers; |
| } |
| |
| /** |
| * Rebuild the list of resolvers. In some cases some parts will need some asynchronous work |
| * to complete. |
| * |
| * @return Whether or not the list building is completed. |
| */ |
| protected boolean rebuildList() { |
| List<ResolvedComponentInfo> currentResolveList = null; |
| // Clear the value of mOtherProfile from previous call. |
| mOtherProfile = null; |
| mLastChosen = null; |
| mLastChosenPosition = -1; |
| mAllTargetsAreBrowsers = false; |
| mDisplayList.clear(); |
| if (mBaseResolveList != null) { |
| currentResolveList = mUnfilteredResolveList = new ArrayList<>(); |
| mResolverListController.addResolveListDedupe(currentResolveList, |
| mResolverListCommunicator.getTargetIntent(), |
| mBaseResolveList); |
| } else { |
| currentResolveList = mUnfilteredResolveList = |
| mResolverListController.getResolversForIntent(shouldGetResolvedFilter(), |
| mResolverListCommunicator.shouldGetActivityMetadata(), |
| mIntents); |
| if (currentResolveList == null) { |
| processSortedList(currentResolveList); |
| return true; |
| } |
| List<ResolvedComponentInfo> originalList = |
| mResolverListController.filterIneligibleActivities(currentResolveList, |
| true); |
| if (originalList != null) { |
| mUnfilteredResolveList = originalList; |
| } |
| } |
| |
| // So far we only support a single other profile at a time. |
| // The first one we see gets special treatment. |
| for (ResolvedComponentInfo info : currentResolveList) { |
| ResolveInfo resolveInfo = info.getResolveInfoAt(0); |
| if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { |
| Intent pOrigIntent = mResolverListCommunicator.getReplacementIntent( |
| resolveInfo.activityInfo, |
| info.getIntentAt(0)); |
| Intent replacementIntent = mResolverListCommunicator.getReplacementIntent( |
| resolveInfo.activityInfo, |
| mResolverListCommunicator.getTargetIntent()); |
| mOtherProfile = new DisplayResolveInfo(info.getIntentAt(0), |
| resolveInfo, |
| resolveInfo.loadLabel(mPm), |
| resolveInfo.loadLabel(mPm), |
| pOrigIntent != null ? pOrigIntent : replacementIntent, |
| makePresentationGetter(resolveInfo)); |
| currentResolveList.remove(info); |
| break; |
| } |
| } |
| |
| if (mOtherProfile == null) { |
| try { |
| mLastChosen = mResolverListController.getLastChosen(); |
| } catch (RemoteException re) { |
| Log.d(TAG, "Error calling getLastChosenActivity\n" + re); |
| } |
| } |
| |
| int n; |
| if ((currentResolveList != null) && ((n = currentResolveList.size()) > 0)) { |
| // We only care about fixing the unfilteredList if the current resolve list and |
| // current resolve list are currently the same. |
| List<ResolvedComponentInfo> originalList = |
| mResolverListController.filterLowPriority(currentResolveList, |
| mUnfilteredResolveList == currentResolveList); |
| if (originalList != null) { |
| mUnfilteredResolveList = originalList; |
| } |
| |
| if (currentResolveList.size() > 1) { |
| int placeholderCount = currentResolveList.size(); |
| if (mResolverListCommunicator.useLayoutWithDefault()) { |
| --placeholderCount; |
| } |
| setPlaceholderCount(placeholderCount); |
| createSortingTask().execute(currentResolveList); |
| postListReadyRunnable(); |
| return false; |
| } else { |
| processSortedList(currentResolveList); |
| return true; |
| } |
| } else { |
| processSortedList(currentResolveList); |
| return true; |
| } |
| } |
| |
| AsyncTask<List<ResolvedComponentInfo>, |
| Void, |
| List<ResolvedComponentInfo>> createSortingTask() { |
| return new AsyncTask<List<ResolvedComponentInfo>, |
| Void, |
| List<ResolvedComponentInfo>>() { |
| @Override |
| protected List<ResolvedComponentInfo> doInBackground( |
| List<ResolvedComponentInfo>... params) { |
| mResolverListController.sort(params[0]); |
| return params[0]; |
| } |
| @Override |
| protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) { |
| processSortedList(sortedComponents); |
| mResolverListCommunicator.updateProfileViewButton(); |
| notifyDataSetChanged(); |
| } |
| }; |
| } |
| |
| |
| protected void processSortedList(List<ResolvedComponentInfo> sortedComponents) { |
| int n; |
| if (sortedComponents != null && (n = sortedComponents.size()) != 0) { |
| mAllTargetsAreBrowsers = mUseLayoutForBrowsables; |
| |
| // First put the initial items at the top. |
| if (mInitialIntents != null) { |
| for (int i = 0; i < mInitialIntents.length; i++) { |
| Intent ii = mInitialIntents[i]; |
| if (ii == null) { |
| continue; |
| } |
| ActivityInfo ai = ii.resolveActivityInfo( |
| mPm, 0); |
| if (ai == null) { |
| Log.w(TAG, "No activity found for " + ii); |
| continue; |
| } |
| ResolveInfo ri = new ResolveInfo(); |
| ri.activityInfo = ai; |
| UserManager userManager = |
| (UserManager) mContext.getSystemService(Context.USER_SERVICE); |
| if (ii instanceof LabeledIntent) { |
| LabeledIntent li = (LabeledIntent) ii; |
| ri.resolvePackageName = li.getSourcePackage(); |
| ri.labelRes = li.getLabelResource(); |
| ri.nonLocalizedLabel = li.getNonLocalizedLabel(); |
| ri.icon = li.getIconResource(); |
| ri.iconResourceId = ri.icon; |
| } |
| if (userManager.isManagedProfile()) { |
| ri.noResourceId = true; |
| ri.icon = 0; |
| } |
| mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; |
| |
| addResolveInfo(new DisplayResolveInfo(ii, ri, |
| ri.loadLabel(mPm), null, ii, makePresentationGetter(ri))); |
| } |
| } |
| |
| |
| for (ResolvedComponentInfo rci : sortedComponents) { |
| final ResolveInfo ri = rci.getResolveInfoAt(0); |
| if (ri != null) { |
| mAllTargetsAreBrowsers &= ri.handleAllWebDataURI; |
| addResolveInfoWithAlternates(rci); |
| } |
| } |
| } |
| |
| mResolverListCommunicator.sendVoiceChoicesIfNeeded(); |
| postListReadyRunnable(); |
| } |
| |
| /** |
| * Some necessary methods for creating the list are initiated in onCreate and will also |
| * determine the layout known. We therefore can't update the UI inline and post to the |
| * handler thread to update after the current task is finished. |
| */ |
| private void postListReadyRunnable() { |
| if (mPostListReadyRunnable == null) { |
| mPostListReadyRunnable = new Runnable() { |
| @Override |
| public void run() { |
| mResolverListCommunicator.onPostListReady(); |
| mPostListReadyRunnable = null; |
| } |
| }; |
| mContext.getMainThreadHandler().post(mPostListReadyRunnable); |
| } |
| } |
| |
| public boolean shouldGetResolvedFilter() { |
| return mFilterLastUsed; |
| } |
| |
| private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) { |
| final int count = rci.getCount(); |
| final Intent intent = rci.getIntentAt(0); |
| final ResolveInfo add = rci.getResolveInfoAt(0); |
| final Intent replaceIntent = |
| mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent); |
| final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent( |
| add.activityInfo, mResolverListCommunicator.getTargetIntent()); |
| final DisplayResolveInfo |
| dri = new DisplayResolveInfo(intent, add, |
| replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add)); |
| dri.setPinned(rci.isPinned()); |
| if (rci.isPinned()) { |
| Log.i(TAG, "Pinned item: " + rci.name); |
| } |
| addResolveInfo(dri); |
| if (replaceIntent == intent) { |
| // Only add alternates if we didn't get a specific replacement from |
| // the caller. If we have one it trumps potential alternates. |
| for (int i = 1, n = count; i < n; i++) { |
| final Intent altIntent = rci.getIntentAt(i); |
| dri.addAlternateSourceIntent(altIntent); |
| } |
| } |
| updateLastChosenPosition(add); |
| } |
| |
| private void updateLastChosenPosition(ResolveInfo info) { |
| // If another profile is present, ignore the last chosen entry. |
| if (mOtherProfile != null) { |
| mLastChosenPosition = -1; |
| return; |
| } |
| if (mLastChosen != null |
| && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName) |
| && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) { |
| mLastChosenPosition = mDisplayList.size() - 1; |
| } |
| } |
| |
| // We assume that at this point we've already filtered out the only intent for a different |
| // targetUserId which we're going to use. |
| private void addResolveInfo(DisplayResolveInfo dri) { |
| if (dri != null && dri.getResolveInfo() != null |
| && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) { |
| // Checks if this info is already listed in display. |
| for (DisplayResolveInfo existingInfo : mDisplayList) { |
| if (mResolverListCommunicator |
| .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) { |
| return; |
| } |
| } |
| mDisplayList.add(dri); |
| } |
| } |
| |
| @Nullable |
| public ResolveInfo resolveInfoForPosition(int position, boolean filtered) { |
| TargetInfo target = targetInfoForPosition(position, filtered); |
| if (target != null) { |
| return target.getResolveInfo(); |
| } |
| return null; |
| } |
| |
| @Nullable |
| public TargetInfo targetInfoForPosition(int position, boolean filtered) { |
| if (filtered) { |
| return getItem(position); |
| } |
| if (mDisplayList.size() > position) { |
| return mDisplayList.get(position); |
| } |
| return null; |
| } |
| |
| public int getCount() { |
| int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount : |
| mDisplayList.size(); |
| if (mFilterLastUsed && mLastChosenPosition >= 0) { |
| totalSize--; |
| } |
| return totalSize; |
| } |
| |
| public int getUnfilteredCount() { |
| return mDisplayList.size(); |
| } |
| |
| @Nullable |
| public TargetInfo getItem(int position) { |
| if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) { |
| position++; |
| } |
| if (mDisplayList.size() > position) { |
| return mDisplayList.get(position); |
| } else { |
| return null; |
| } |
| } |
| |
| public long getItemId(int position) { |
| return position; |
| } |
| |
| public int getDisplayResolveInfoCount() { |
| return mDisplayList.size(); |
| } |
| |
| public DisplayResolveInfo getDisplayResolveInfo(int index) { |
| // Used to query services. We only query services for primary targets, not alternates. |
| return mDisplayList.get(index); |
| } |
| |
| public final View getView(int position, View convertView, ViewGroup parent) { |
| View view = convertView; |
| if (view == null) { |
| view = createView(parent); |
| } |
| onBindView(view, getItem(position)); |
| return view; |
| } |
| |
| public final View createView(ViewGroup parent) { |
| final View view = onCreateView(parent); |
| final ViewHolder holder = new ViewHolder(view); |
| view.setTag(holder); |
| return view; |
| } |
| |
| View onCreateView(ViewGroup parent) { |
| return mInflater.inflate( |
| com.android.internal.R.layout.resolve_list_item, parent, false); |
| } |
| |
| public final void bindView(int position, View view) { |
| onBindView(view, getItem(position)); |
| } |
| |
| protected void onBindView(View view, TargetInfo info) { |
| final ViewHolder holder = (ViewHolder) view.getTag(); |
| if (info == null) { |
| holder.icon.setImageDrawable( |
| mContext.getDrawable(R.drawable.resolver_icon_placeholder)); |
| return; |
| } |
| |
| if (info instanceof DisplayResolveInfo |
| && !((DisplayResolveInfo) info).hasDisplayLabel()) { |
| getLoadLabelTask((DisplayResolveInfo) info, holder).execute(); |
| } else { |
| holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo()); |
| } |
| |
| if (info.isSuspended()) { |
| holder.icon.setColorFilter(mSuspendedMatrixColorFilter); |
| } else { |
| holder.icon.setColorFilter(null); |
| } |
| |
| if (info instanceof DisplayResolveInfo |
| && !((DisplayResolveInfo) info).hasDisplayIcon()) { |
| new ResolverListAdapter.LoadIconTask((DisplayResolveInfo) info, holder.icon).execute(); |
| } else { |
| holder.icon.setImageDrawable(info.getDisplayIcon(mContext)); |
| } |
| } |
| |
| protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) { |
| return new LoadLabelTask(info, holder); |
| } |
| |
| public void onDestroy() { |
| if (mPostListReadyRunnable != null) { |
| mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable); |
| mPostListReadyRunnable = null; |
| } |
| if (mResolverListController != null) { |
| mResolverListController.destroy(); |
| } |
| } |
| |
| private ColorMatrixColorFilter createSuspendedColorMatrix() { |
| int grayValue = 127; |
| float scale = 0.5f; // half bright |
| |
| ColorMatrix tempBrightnessMatrix = new ColorMatrix(); |
| float[] mat = tempBrightnessMatrix.getArray(); |
| mat[0] = scale; |
| mat[6] = scale; |
| mat[12] = scale; |
| mat[4] = grayValue; |
| mat[9] = grayValue; |
| mat[14] = grayValue; |
| |
| ColorMatrix matrix = new ColorMatrix(); |
| matrix.setSaturation(0.0f); |
| matrix.preConcat(tempBrightnessMatrix); |
| return new ColorMatrixColorFilter(matrix); |
| } |
| |
| ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) { |
| return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai); |
| } |
| |
| ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) { |
| return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri); |
| } |
| |
| Drawable loadIconForResolveInfo(ResolveInfo ri) { |
| // Load icons based on the current process. If in work profile icons should be badged. |
| return makePresentationGetter(ri).getIcon(Process.myUserHandle()); |
| } |
| |
| void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) { |
| final DisplayResolveInfo iconInfo = getFilteredItem(); |
| if (iconView != null && iconInfo != null) { |
| new LoadIconTask(iconInfo, iconView).execute(); |
| } |
| } |
| |
| /** |
| * Necessary methods to communicate between {@link ResolverListAdapter} |
| * and {@link ResolverActivity}. |
| */ |
| interface ResolverListCommunicator { |
| |
| boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs); |
| |
| Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent); |
| |
| void onPostListReady(); |
| |
| void sendVoiceChoicesIfNeeded(); |
| |
| void updateProfileViewButton(); |
| |
| boolean useLayoutWithDefault(); |
| |
| boolean shouldGetActivityMetadata(); |
| |
| Intent getTargetIntent(); |
| |
| void onHandlePackagesChanged(); |
| } |
| |
| static class ViewHolder { |
| public View itemView; |
| public Drawable defaultItemViewBackground; |
| |
| public TextView text; |
| public TextView text2; |
| public ImageView icon; |
| |
| ViewHolder(View view) { |
| itemView = view; |
| defaultItemViewBackground = view.getBackground(); |
| text = (TextView) view.findViewById(com.android.internal.R.id.text1); |
| text2 = (TextView) view.findViewById(com.android.internal.R.id.text2); |
| icon = (ImageView) view.findViewById(R.id.icon); |
| } |
| |
| public void bindLabel(CharSequence label, CharSequence subLabel) { |
| if (!TextUtils.equals(text.getText(), label)) { |
| text.setText(label); |
| } |
| |
| // Always show a subLabel for visual consistency across list items. Show an empty |
| // subLabel if the subLabel is the same as the label |
| if (TextUtils.equals(label, subLabel)) { |
| subLabel = null; |
| } |
| |
| if (!TextUtils.equals(text2.getText(), subLabel)) { |
| text2.setVisibility(View.VISIBLE); |
| text2.setText(subLabel); |
| } |
| } |
| } |
| |
| protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> { |
| private final DisplayResolveInfo mDisplayResolveInfo; |
| private final ViewHolder mHolder; |
| |
| protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) { |
| mDisplayResolveInfo = dri; |
| mHolder = holder; |
| } |
| |
| @Override |
| protected CharSequence[] doInBackground(Void... voids) { |
| ResolveInfoPresentationGetter pg = |
| makePresentationGetter(mDisplayResolveInfo.getResolveInfo()); |
| |
| if (mIsAudioCaptureDevice) { |
| // This is an audio capture device, so check record permissions |
| ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo; |
| String packageName = activityInfo.packageName; |
| |
| int uid = activityInfo.applicationInfo.uid; |
| boolean hasRecordPermission = |
| PermissionChecker.checkPermissionForPreflight( |
| mContext, |
| android.Manifest.permission.RECORD_AUDIO, -1, uid, |
| packageName) |
| == android.content.pm.PackageManager.PERMISSION_GRANTED; |
| |
| if (!hasRecordPermission) { |
| // Doesn't have record permission, so warn the user |
| return new CharSequence[] { |
| pg.getLabel(), |
| mContext.getString(R.string.usb_device_resolve_prompt_warn) |
| }; |
| } |
| } |
| |
| return new CharSequence[] { |
| pg.getLabel(), |
| pg.getSubLabel() |
| }; |
| } |
| |
| @Override |
| protected void onPostExecute(CharSequence[] result) { |
| mDisplayResolveInfo.setDisplayLabel(result[0]); |
| mDisplayResolveInfo.setExtendedInfo(result[1]); |
| mHolder.bindLabel(result[0], result[1]); |
| } |
| } |
| |
| class LoadIconTask extends AsyncTask<Void, Void, Drawable> { |
| protected final com.android.internal.app.chooser.DisplayResolveInfo mDisplayResolveInfo; |
| private final ResolveInfo mResolveInfo; |
| private final ImageView mTargetView; |
| |
| LoadIconTask(DisplayResolveInfo dri, ImageView target) { |
| mDisplayResolveInfo = dri; |
| mResolveInfo = dri.getResolveInfo(); |
| mTargetView = target; |
| } |
| |
| @Override |
| protected Drawable doInBackground(Void... params) { |
| return loadIconForResolveInfo(mResolveInfo); |
| } |
| |
| @Override |
| protected void onPostExecute(Drawable d) { |
| if (getOtherProfile() == mDisplayResolveInfo) { |
| mResolverListCommunicator.updateProfileViewButton(); |
| } else { |
| mDisplayResolveInfo.setDisplayIcon(d); |
| mTargetView.setImageDrawable(d); |
| } |
| } |
| } |
| |
| /** |
| * Loads the icon and label for the provided ResolveInfo. |
| */ |
| @VisibleForTesting |
| public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter { |
| private final ResolveInfo mRi; |
| public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) { |
| super(ctx, iconDpi, ri.activityInfo); |
| mRi = ri; |
| } |
| |
| @Override |
| Drawable getIconSubstituteInternal() { |
| Drawable dr = null; |
| try { |
| // Do not use ResolveInfo#getIconResource() as it defaults to the app |
| if (mRi.resolvePackageName != null && mRi.icon != 0) { |
| dr = loadIconFromResource( |
| mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " |
| + "couldn't find resources for package", e); |
| } |
| |
| // Fall back to ActivityInfo if no icon is found via ResolveInfo |
| if (dr == null) dr = super.getIconSubstituteInternal(); |
| |
| return dr; |
| } |
| |
| @Override |
| String getAppSubLabelInternal() { |
| // Will default to app name if no intent filter or activity label set, make sure to |
| // check if subLabel matches label before final display |
| return (String) mRi.loadLabel(mPm); |
| } |
| } |
| |
| /** |
| * Loads the icon and label for the provided ActivityInfo. |
| */ |
| @VisibleForTesting |
| public static class ActivityInfoPresentationGetter extends |
| TargetPresentationGetter { |
| private final ActivityInfo mActivityInfo; |
| public ActivityInfoPresentationGetter(Context ctx, int iconDpi, |
| ActivityInfo activityInfo) { |
| super(ctx, iconDpi, activityInfo.applicationInfo); |
| mActivityInfo = activityInfo; |
| } |
| |
| @Override |
| Drawable getIconSubstituteInternal() { |
| Drawable dr = null; |
| try { |
| // Do not use ActivityInfo#getIconResource() as it defaults to the app |
| if (mActivityInfo.icon != 0) { |
| dr = loadIconFromResource( |
| mPm.getResourcesForApplication(mActivityInfo.applicationInfo), |
| mActivityInfo.icon); |
| } |
| } catch (PackageManager.NameNotFoundException e) { |
| Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but " |
| + "couldn't find resources for package", e); |
| } |
| |
| return dr; |
| } |
| |
| @Override |
| String getAppSubLabelInternal() { |
| // Will default to app name if no activity label set, make sure to check if subLabel |
| // matches label before final display |
| return (String) mActivityInfo.loadLabel(mPm); |
| } |
| } |
| |
| /** |
| * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application |
| * icon and label over any IntentFilter or Activity icon to increase user understanding, with an |
| * exception for applications that hold the right permission. Always attempts to use available |
| * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses |
| * Strings to strip creative formatting. |
| */ |
| private abstract static class TargetPresentationGetter { |
| @Nullable abstract Drawable getIconSubstituteInternal(); |
| @Nullable abstract String getAppSubLabelInternal(); |
| |
| private Context mCtx; |
| private final int mIconDpi; |
| private final boolean mHasSubstitutePermission; |
| private final ApplicationInfo mAi; |
| |
| protected PackageManager mPm; |
| |
| TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) { |
| mCtx = ctx; |
| mPm = ctx.getPackageManager(); |
| mAi = ai; |
| mIconDpi = iconDpi; |
| mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission( |
| android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON, |
| mAi.packageName); |
| } |
| |
| public Drawable getIcon(UserHandle userHandle) { |
| return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle)); |
| } |
| |
| public Bitmap getIconBitmap(UserHandle userHandle) { |
| Drawable dr = null; |
| if (mHasSubstitutePermission) { |
| dr = getIconSubstituteInternal(); |
| } |
| |
| if (dr == null) { |
| try { |
| if (mAi.icon != 0) { |
| dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon); |
| } |
| } catch (PackageManager.NameNotFoundException ignore) { |
| } |
| } |
| |
| // Fall back to ApplicationInfo#loadIcon if nothing has been loaded |
| if (dr == null) { |
| dr = mAi.loadIcon(mPm); |
| } |
| |
| SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx); |
| Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle); |
| sif.recycle(); |
| |
| return icon; |
| } |
| |
| public String getLabel() { |
| String label = null; |
| // Apps with the substitute permission will always show the sublabel as their label |
| if (mHasSubstitutePermission) { |
| label = getAppSubLabelInternal(); |
| } |
| |
| if (label == null) { |
| label = (String) mAi.loadLabel(mPm); |
| } |
| |
| return label; |
| } |
| |
| public String getSubLabel() { |
| // Apps with the substitute permission will never have a sublabel |
| if (mHasSubstitutePermission) return null; |
| return getAppSubLabelInternal(); |
| } |
| |
| protected String loadLabelFromResource(Resources res, int resId) { |
| return res.getString(resId); |
| } |
| |
| @Nullable |
| protected Drawable loadIconFromResource(Resources res, int resId) { |
| return res.getDrawableForDensity(resId, mIconDpi); |
| } |
| |
| } |
| } |