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