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