1 /* 2 * Copyright (C) 2016 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 18 package com.android.intentresolver; 19 20 import static android.content.Context.ACTIVITY_SERVICE; 21 22 import static java.util.stream.Collectors.toList; 23 24 import android.app.ActivityManager; 25 import android.app.Dialog; 26 import android.content.ComponentName; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.IntentFilter; 30 import android.content.SharedPreferences; 31 import android.content.pm.LauncherApps; 32 import android.content.pm.PackageManager; 33 import android.content.pm.ShortcutInfo; 34 import android.content.pm.ShortcutManager; 35 import android.graphics.Color; 36 import android.graphics.drawable.BitmapDrawable; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.Drawable; 39 import android.os.Bundle; 40 import android.os.UserHandle; 41 import android.util.Pair; 42 import android.view.LayoutInflater; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.widget.ImageView; 46 import android.widget.TextView; 47 48 import androidx.annotation.NonNull; 49 import androidx.annotation.Nullable; 50 import androidx.fragment.app.DialogFragment; 51 import androidx.fragment.app.FragmentManager; 52 import androidx.recyclerview.widget.RecyclerView; 53 54 import com.android.intentresolver.chooser.DisplayResolveInfo; 55 56 import java.util.List; 57 import java.util.Optional; 58 import java.util.stream.Collectors; 59 60 /** 61 * Shows a dialog with actions to take on a chooser target. 62 */ 63 public class ChooserTargetActionsDialogFragment extends DialogFragment 64 implements DialogInterface.OnClickListener { 65 66 protected final static String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment"; 67 68 private final List<DisplayResolveInfo> mTargetInfos; 69 private final UserHandle mUserHandle; 70 private final boolean mIsShortcutPinned; 71 72 @Nullable 73 private final String mShortcutId; 74 75 @Nullable 76 private final String mShortcutTitle; 77 78 @Nullable 79 private final IntentFilter mIntentFilter; 80 show( FragmentManager fragmentManager, List<DisplayResolveInfo> targetInfos, UserHandle userHandle, @Nullable String shortcutId, @Nullable String shortcutTitle, boolean isShortcutPinned, @Nullable IntentFilter intentFilter)81 public static void show( 82 FragmentManager fragmentManager, 83 List<DisplayResolveInfo> targetInfos, 84 UserHandle userHandle, 85 @Nullable String shortcutId, 86 @Nullable String shortcutTitle, 87 boolean isShortcutPinned, 88 @Nullable IntentFilter intentFilter) { 89 ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment( 90 targetInfos, 91 userHandle, 92 shortcutId, 93 shortcutTitle, 94 isShortcutPinned, 95 intentFilter); 96 fragment.show(fragmentManager, TARGET_DETAILS_FRAGMENT_TAG); 97 } 98 99 @Override onCreate(Bundle savedInstanceState)100 public void onCreate(Bundle savedInstanceState) { 101 super.onCreate(savedInstanceState); 102 103 if (savedInstanceState != null) { 104 // Bail. It's probably not possible to trigger reloading our fragments from a saved 105 // instance since Sharesheet isn't kept in history and the entire session will probably 106 // be lost under any conditions that would've triggered our retention. Nevertheless, if 107 // we ever *did* try to load from a saved state, we wouldn't be able to populate valid 108 // data (since we wouldn't be able to get back our original TargetInfos if we had to 109 // restore them from a Bundle). 110 dismissAllowingStateLoss(); 111 } 112 } 113 114 /** 115 * Build the menu UI according to our design spec. 116 */ 117 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState)118 public View onCreateView(LayoutInflater inflater, 119 @Nullable ViewGroup container, 120 Bundle savedInstanceState) { 121 // Make the background transparent to show dialog rounding 122 Optional.of(getDialog()).map(Dialog::getWindow) 123 .ifPresent(window -> { 124 window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 125 }); 126 127 // Fetch UI details from target info 128 List<Pair<Drawable, CharSequence>> items = mTargetInfos.stream().map(dri -> { 129 return new Pair<>(getItemIcon(dri), getItemLabel(dri)); 130 }).collect(toList()); 131 132 View v = inflater.inflate(R.layout.chooser_dialog, container, false); 133 134 TextView title = v.findViewById(com.android.internal.R.id.title); 135 ImageView icon = v.findViewById(com.android.internal.R.id.icon); 136 RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer); 137 138 final TargetPresentationGetter pg = getProvidingAppPresentationGetter(); 139 title.setText(isShortcutTarget() ? mShortcutTitle : pg.getLabel()); 140 icon.setImageDrawable(new BitmapDrawable(getResources(), pg.getIconBitmap(mUserHandle))); 141 rv.setAdapter(new VHAdapter(items)); 142 143 return v; 144 } 145 146 @Override onStop()147 public void onStop() { 148 super.onStop(); 149 dismissAllowingStateLoss(); 150 } 151 152 class VHAdapter extends RecyclerView.Adapter<VH> { 153 154 List<Pair<Drawable, CharSequence>> mItems; 155 VHAdapter(List<Pair<Drawable, CharSequence>> items)156 VHAdapter(List<Pair<Drawable, CharSequence>> items) { 157 mItems = items; 158 } 159 160 @NonNull 161 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)162 public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 163 return new VH(LayoutInflater.from(parent.getContext()).inflate( 164 R.layout.chooser_dialog_item, parent, false)); 165 } 166 167 @Override onBindViewHolder(@onNull VH holder, int position)168 public void onBindViewHolder(@NonNull VH holder, int position) { 169 holder.bind(mItems.get(position), position); 170 } 171 172 @Override getItemCount()173 public int getItemCount() { 174 return mItems.size(); 175 } 176 } 177 178 class VH extends RecyclerView.ViewHolder { 179 TextView mLabel; 180 ImageView mIcon; 181 VH(@onNull View itemView)182 VH(@NonNull View itemView) { 183 super(itemView); 184 mLabel = itemView.findViewById(com.android.internal.R.id.text); 185 mIcon = itemView.findViewById(com.android.internal.R.id.icon); 186 } 187 bind(Pair<Drawable, CharSequence> item, int position)188 public void bind(Pair<Drawable, CharSequence> item, int position) { 189 mLabel.setText(item.second); 190 191 if (item.first == null) { 192 mIcon.setVisibility(View.GONE); 193 } else { 194 mIcon.setVisibility(View.VISIBLE); 195 mIcon.setImageDrawable(item.first); 196 } 197 198 itemView.setOnClickListener(v -> onClick(getDialog(), position)); 199 } 200 } 201 202 @Override onClick(DialogInterface dialog, int which)203 public void onClick(DialogInterface dialog, int which) { 204 if (isShortcutTarget()) { 205 toggleShortcutPinned(mTargetInfos.get(which).getResolvedComponentName()); 206 } else { 207 pinComponent(mTargetInfos.get(which).getResolvedComponentName()); 208 } 209 ((PackagesChangedListener) getActivity()).handlePackagesChanged(); 210 dismiss(); 211 } 212 toggleShortcutPinned(ComponentName name)213 private void toggleShortcutPinned(ComponentName name) { 214 if (mIntentFilter == null) { 215 return; 216 } 217 // Fetch existing pinned shortcuts of the given package. 218 List<String> pinnedShortcuts = getPinnedShortcutsFromPackageAsUser(getContext(), 219 mUserHandle, mIntentFilter, name.getPackageName()); 220 // If the shortcut has already been pinned, unpin it; otherwise, pin it. 221 if (mIsShortcutPinned) { 222 pinnedShortcuts.remove(mShortcutId); 223 } else { 224 pinnedShortcuts.add(mShortcutId); 225 } 226 // Update pinned shortcut list in ShortcutService via LauncherApps 227 getContext().getSystemService(LauncherApps.class).pinShortcuts( 228 name.getPackageName(), pinnedShortcuts, mUserHandle); 229 } 230 getPinnedShortcutsFromPackageAsUser(Context context, UserHandle user, IntentFilter filter, String packageName)231 private static List<String> getPinnedShortcutsFromPackageAsUser(Context context, 232 UserHandle user, IntentFilter filter, String packageName) { 233 Context contextAsUser = context.createContextAsUser(user, 0 /* flags */); 234 List<ShortcutManager.ShareShortcutInfo> targets = contextAsUser.getSystemService( 235 ShortcutManager.class).getShareTargets(filter); 236 return targets.stream() 237 .map(ShortcutManager.ShareShortcutInfo::getShortcutInfo) 238 .filter(s -> s.isPinned() && s.getPackage().equals(packageName)) 239 .map(ShortcutInfo::getId) 240 .collect(Collectors.toList()); 241 } 242 pinComponent(ComponentName name)243 private void pinComponent(ComponentName name) { 244 SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext()); 245 final String key = name.flattenToString(); 246 boolean currentVal = sp.getBoolean(name.flattenToString(), false); 247 if (currentVal) { 248 sp.edit().remove(key).apply(); 249 } else { 250 sp.edit().putBoolean(key, true).apply(); 251 } 252 } 253 getPinIcon(boolean isPinned)254 private Drawable getPinIcon(boolean isPinned) { 255 return isPinned 256 ? getContext().getDrawable(com.android.internal.R.drawable.ic_close) 257 : getContext().getDrawable(R.drawable.ic_chooser_pin_dialog); 258 } 259 getPinLabel(boolean isPinned, CharSequence targetLabel)260 private CharSequence getPinLabel(boolean isPinned, CharSequence targetLabel) { 261 return isPinned 262 ? getResources().getString(R.string.unpin_specific_target, targetLabel) 263 : getResources().getString(R.string.pin_specific_target, targetLabel); 264 } 265 266 @NonNull getItemLabel(DisplayResolveInfo dri)267 protected CharSequence getItemLabel(DisplayResolveInfo dri) { 268 final PackageManager pm = getContext().getPackageManager(); 269 return getPinLabel(isPinned(dri), 270 isShortcutTarget() ? mShortcutTitle : dri.getResolveInfo().loadLabel(pm)); 271 } 272 273 @Nullable getItemIcon(DisplayResolveInfo dri)274 protected Drawable getItemIcon(DisplayResolveInfo dri) { 275 return getPinIcon(isPinned(dri)); 276 } 277 getProvidingAppPresentationGetter()278 private TargetPresentationGetter getProvidingAppPresentationGetter() { 279 final ActivityManager am = (ActivityManager) getContext() 280 .getSystemService(ACTIVITY_SERVICE); 281 final int iconDpi = am.getLauncherLargeIconDensity(); 282 283 // Use the matching application icon and label for the title, any TargetInfo will do 284 final Context context = getContext(); 285 return new TargetPresentationGetter.Factory( 286 () -> SimpleIconFactory.obtain(context), 287 context.getPackageManager(), 288 iconDpi) 289 .makePresentationGetter(mTargetInfos.get(0).getResolveInfo()); 290 } 291 isPinned(DisplayResolveInfo dri)292 private boolean isPinned(DisplayResolveInfo dri) { 293 return isShortcutTarget() ? mIsShortcutPinned : dri.isPinned(); 294 } 295 isShortcutTarget()296 private boolean isShortcutTarget() { 297 return mShortcutId != null; 298 } 299 ChooserTargetActionsDialogFragment( List<DisplayResolveInfo> targetInfos, UserHandle userHandle)300 protected ChooserTargetActionsDialogFragment( 301 List<DisplayResolveInfo> targetInfos, UserHandle userHandle) { 302 this(targetInfos, userHandle, null, null, false, null); 303 } 304 ChooserTargetActionsDialogFragment( List<DisplayResolveInfo> targetInfos, UserHandle userHandle, @Nullable String shortcutId, @Nullable String shortcutTitle, boolean isShortcutPinned, @Nullable IntentFilter intentFilter)305 private ChooserTargetActionsDialogFragment( 306 List<DisplayResolveInfo> targetInfos, 307 UserHandle userHandle, 308 @Nullable String shortcutId, 309 @Nullable String shortcutTitle, 310 boolean isShortcutPinned, 311 @Nullable IntentFilter intentFilter) { 312 mTargetInfos = targetInfos; 313 mUserHandle = userHandle; 314 mShortcutId = shortcutId; 315 mShortcutTitle = shortcutTitle; 316 mIsShortcutPinned = isShortcutPinned; 317 mIntentFilter = intentFilter; 318 } 319 } 320