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