• 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.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