1 /* 2 * Copyright (C) 2019 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 package com.android.internal.app.chooser; 18 19 import android.annotation.Nullable; 20 import android.app.Activity; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ActivityInfo; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.LauncherApps; 27 import android.content.pm.PackageManager; 28 import android.content.pm.ResolveInfo; 29 import android.content.pm.ShortcutInfo; 30 import android.graphics.Bitmap; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.graphics.drawable.Drawable; 33 import android.graphics.drawable.Icon; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.service.chooser.ChooserTarget; 37 import android.text.SpannableStringBuilder; 38 import android.util.Log; 39 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.app.ChooserActivity; 42 import com.android.internal.app.ResolverActivity; 43 import com.android.internal.app.ResolverListAdapter.ActivityInfoPresentationGetter; 44 import com.android.internal.app.SimpleIconFactory; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 49 /** 50 * Live target, currently selectable by the user. 51 * @see NotSelectableTargetInfo 52 */ 53 public final class SelectableTargetInfo implements ChooserTargetInfo { 54 private static final String TAG = "SelectableTargetInfo"; 55 56 private final Context mContext; 57 private final DisplayResolveInfo mSourceInfo; 58 private final ResolveInfo mBackupResolveInfo; 59 private final ChooserTarget mChooserTarget; 60 private final String mDisplayLabel; 61 private final PackageManager mPm; 62 private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator; 63 @GuardedBy("this") 64 private ShortcutInfo mShortcutInfo; 65 private Drawable mBadgeIcon = null; 66 private CharSequence mBadgeContentDescription; 67 @GuardedBy("this") 68 private Drawable mDisplayIcon; 69 private final Intent mFillInIntent; 70 private final int mFillInFlags; 71 private final boolean mIsPinned; 72 private final float mModifiedScore; 73 private boolean mIsSuspended = false; 74 SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo, ChooserTarget chooserTarget, float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator, @Nullable ShortcutInfo shortcutInfo)75 public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo, 76 ChooserTarget chooserTarget, 77 float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator, 78 @Nullable ShortcutInfo shortcutInfo) { 79 mContext = context; 80 mSourceInfo = sourceInfo; 81 mChooserTarget = chooserTarget; 82 mModifiedScore = modifiedScore; 83 mPm = mContext.getPackageManager(); 84 mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator; 85 mShortcutInfo = shortcutInfo; 86 mIsPinned = shortcutInfo != null && shortcutInfo.isPinned(); 87 if (sourceInfo != null) { 88 final ResolveInfo ri = sourceInfo.getResolveInfo(); 89 if (ri != null) { 90 final ActivityInfo ai = ri.activityInfo; 91 if (ai != null && ai.applicationInfo != null) { 92 final PackageManager pm = mContext.getPackageManager(); 93 mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo); 94 mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo); 95 mIsSuspended = 96 (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0; 97 } 98 } 99 } 100 101 if (sourceInfo != null) { 102 mBackupResolveInfo = null; 103 } else { 104 mBackupResolveInfo = 105 mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0); 106 } 107 108 mFillInIntent = null; 109 mFillInFlags = 0; 110 111 mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle()); 112 } 113 SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags)114 private SelectableTargetInfo(SelectableTargetInfo other, 115 Intent fillInIntent, int flags) { 116 mContext = other.mContext; 117 mPm = other.mPm; 118 mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator; 119 mSourceInfo = other.mSourceInfo; 120 mBackupResolveInfo = other.mBackupResolveInfo; 121 mChooserTarget = other.mChooserTarget; 122 mBadgeIcon = other.mBadgeIcon; 123 mBadgeContentDescription = other.mBadgeContentDescription; 124 synchronized (other) { 125 mShortcutInfo = other.mShortcutInfo; 126 mDisplayIcon = other.mDisplayIcon; 127 } 128 mFillInIntent = fillInIntent; 129 mFillInFlags = flags; 130 mModifiedScore = other.mModifiedScore; 131 mIsPinned = other.mIsPinned; 132 133 mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle()); 134 } 135 sanitizeDisplayLabel(CharSequence label)136 private String sanitizeDisplayLabel(CharSequence label) { 137 SpannableStringBuilder sb = new SpannableStringBuilder(label); 138 sb.clearSpans(); 139 return sb.toString(); 140 } 141 isSuspended()142 public boolean isSuspended() { 143 return mIsSuspended; 144 } 145 getDisplayResolveInfo()146 public DisplayResolveInfo getDisplayResolveInfo() { 147 return mSourceInfo; 148 } 149 150 /** 151 * Load display icon, if needed. 152 */ loadIcon()153 public boolean loadIcon() { 154 ShortcutInfo shortcutInfo; 155 Drawable icon; 156 synchronized (this) { 157 shortcutInfo = mShortcutInfo; 158 icon = mDisplayIcon; 159 } 160 boolean shouldLoadIcon = icon == null && shortcutInfo != null; 161 if (shouldLoadIcon) { 162 icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo); 163 synchronized (this) { 164 mDisplayIcon = icon; 165 mShortcutInfo = null; 166 } 167 } 168 return shouldLoadIcon; 169 } 170 getChooserTargetIconDrawable(ChooserTarget target, @Nullable ShortcutInfo shortcutInfo)171 private Drawable getChooserTargetIconDrawable(ChooserTarget target, 172 @Nullable ShortcutInfo shortcutInfo) { 173 Drawable directShareIcon = null; 174 175 // First get the target drawable and associated activity info 176 final Icon icon = target.getIcon(); 177 if (icon != null) { 178 directShareIcon = icon.loadDrawable(mContext); 179 } else if (shortcutInfo != null) { 180 LauncherApps launcherApps = (LauncherApps) mContext.getSystemService( 181 Context.LAUNCHER_APPS_SERVICE); 182 directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0); 183 } 184 185 if (directShareIcon == null) return null; 186 187 ActivityInfo info = null; 188 try { 189 info = mPm.getActivityInfo(target.getComponentName(), 0); 190 } catch (PackageManager.NameNotFoundException error) { 191 Log.e(TAG, "Could not find activity associated with ChooserTarget"); 192 } 193 194 if (info == null) return null; 195 196 // Now fetch app icon and raster with no badging even in work profile 197 Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info) 198 .getIconBitmap(null); 199 200 // Raster target drawable with appIcon as a badge 201 SimpleIconFactory sif = SimpleIconFactory.obtain(mContext); 202 Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon); 203 sif.recycle(); 204 205 return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon); 206 } 207 getModifiedScore()208 public float getModifiedScore() { 209 return mModifiedScore; 210 } 211 212 @Override getResolvedIntent()213 public Intent getResolvedIntent() { 214 if (mSourceInfo != null) { 215 return mSourceInfo.getResolvedIntent(); 216 } 217 218 final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent()); 219 targetIntent.setComponent(mChooserTarget.getComponentName()); 220 targetIntent.putExtras(mChooserTarget.getIntentExtras()); 221 return targetIntent; 222 } 223 224 @Override getResolvedComponentName()225 public ComponentName getResolvedComponentName() { 226 if (mSourceInfo != null) { 227 return mSourceInfo.getResolvedComponentName(); 228 } else if (mBackupResolveInfo != null) { 229 return new ComponentName(mBackupResolveInfo.activityInfo.packageName, 230 mBackupResolveInfo.activityInfo.name); 231 } 232 return null; 233 } 234 getBaseIntentToSend()235 private Intent getBaseIntentToSend() { 236 Intent result = getResolvedIntent(); 237 if (result == null) { 238 Log.e(TAG, "ChooserTargetInfo: no base intent available to send"); 239 } else { 240 result = new Intent(result); 241 if (mFillInIntent != null) { 242 result.fillIn(mFillInIntent, mFillInFlags); 243 } 244 result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0); 245 } 246 return result; 247 } 248 249 @Override start(Activity activity, Bundle options)250 public boolean start(Activity activity, Bundle options) { 251 throw new RuntimeException("ChooserTargets should be started as caller."); 252 } 253 254 @Override startAsCaller(ResolverActivity activity, Bundle options, int userId)255 public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) { 256 final Intent intent = getBaseIntentToSend(); 257 if (intent == null) { 258 return false; 259 } 260 intent.setComponent(mChooserTarget.getComponentName()); 261 intent.putExtras(mChooserTarget.getIntentExtras()); 262 TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId); 263 264 // Important: we will ignore the target security checks in ActivityManager 265 // if and only if the ChooserTarget's target package is the same package 266 // where we got the ChooserTargetService that provided it. This lets a 267 // ChooserTargetService provide a non-exported or permission-guarded target 268 // to the chooser for the user to pick. 269 // 270 // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere 271 // so we'll obey the caller's normal security checks. 272 final boolean ignoreTargetSecurity = mSourceInfo != null 273 && mSourceInfo.getResolvedComponentName().getPackageName() 274 .equals(mChooserTarget.getComponentName().getPackageName()); 275 activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId); 276 return true; 277 } 278 279 @Override startAsUser(Activity activity, Bundle options, UserHandle user)280 public boolean startAsUser(Activity activity, Bundle options, UserHandle user) { 281 throw new RuntimeException("ChooserTargets should be started as caller."); 282 } 283 284 @Override getResolveInfo()285 public ResolveInfo getResolveInfo() { 286 return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo; 287 } 288 289 @Override getDisplayLabel()290 public CharSequence getDisplayLabel() { 291 return mDisplayLabel; 292 } 293 294 @Override getExtendedInfo()295 public CharSequence getExtendedInfo() { 296 // ChooserTargets have badge icons, so we won't show the extended info to disambiguate. 297 return null; 298 } 299 300 @Override getDisplayIcon(Context context)301 public synchronized Drawable getDisplayIcon(Context context) { 302 return mDisplayIcon; 303 } 304 305 /** 306 * @return true if display icon is available 307 */ hasDisplayIcon()308 public synchronized boolean hasDisplayIcon() { 309 return mDisplayIcon != null; 310 } 311 getChooserTarget()312 public ChooserTarget getChooserTarget() { 313 return mChooserTarget; 314 } 315 316 @Override cloneFilledIn(Intent fillInIntent, int flags)317 public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) { 318 return new SelectableTargetInfo(this, fillInIntent, flags); 319 } 320 321 @Override getAllSourceIntents()322 public List<Intent> getAllSourceIntents() { 323 final List<Intent> results = new ArrayList<>(); 324 if (mSourceInfo != null) { 325 // We only queried the service for the first one in our sourceinfo. 326 results.add(mSourceInfo.getAllSourceIntents().get(0)); 327 } 328 return results; 329 } 330 331 @Override isPinned()332 public boolean isPinned() { 333 return mIsPinned; 334 } 335 336 /** 337 * Necessary methods to communicate between {@link SelectableTargetInfo} 338 * and {@link ResolverActivity} or {@link ChooserActivity}. 339 */ 340 public interface SelectableTargetInfoCommunicator { 341 makePresentationGetter(ActivityInfo info)342 ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info); 343 getTargetIntent()344 Intent getTargetIntent(); 345 getReferrerFillInIntent()346 Intent getReferrerFillInIntent(); 347 } 348 } 349