• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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