1 /*
2  * Copyright (C) 2020 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 package com.android.launcher3.secondarydisplay;
17 
18 import static android.content.Context.MODE_PRIVATE;
19 
20 import android.content.ComponentName;
21 import android.content.SharedPreferences;
22 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
23 import android.os.Process;
24 import android.os.UserHandle;
25 import android.view.LayoutInflater;
26 import android.view.View;
27 import android.view.View.OnClickListener;
28 import android.view.View.OnLongClickListener;
29 import android.view.ViewGroup;
30 import android.widget.BaseAdapter;
31 
32 import com.android.launcher3.AbstractFloatingView;
33 import com.android.launcher3.BubbleTextView;
34 import com.android.launcher3.R;
35 import com.android.launcher3.allapps.AllAppsStore;
36 import com.android.launcher3.allapps.AppInfoComparator;
37 import com.android.launcher3.model.data.AppInfo;
38 import com.android.launcher3.model.data.ItemInfo;
39 import com.android.launcher3.pm.UserCache;
40 import com.android.launcher3.popup.SystemShortcut;
41 import com.android.launcher3.util.ComponentKey;
42 import com.android.launcher3.util.Executors;
43 
44 import java.util.ArrayList;
45 import java.util.Collections;
46 import java.util.HashSet;
47 import java.util.Objects;
48 import java.util.Set;
49 import java.util.function.Function;
50 import java.util.stream.Collectors;
51 
52 /**
53  * Adapter to manage pinned apps and show then in a grid.
54  */
55 public class PinnedAppsAdapter extends BaseAdapter implements OnSharedPreferenceChangeListener {
56 
57     private static final String PINNED_APPS_KEY = "pinned_apps";
58 
59     private final SecondaryDisplayLauncher mLauncher;
60     private final OnClickListener mOnClickListener;
61     private final OnLongClickListener mOnLongClickListener;
62     private final SharedPreferences mPrefs;
63     private final AllAppsStore<SecondaryDisplayLauncher> mAllAppsList;
64     private final AppInfoComparator mAppNameComparator;
65 
66     private final Set<ComponentKey> mPinnedApps = new HashSet<>();
67     private final ArrayList<AppInfo> mItems = new ArrayList<>();
68 
PinnedAppsAdapter( SecondaryDisplayLauncher launcher, AllAppsStore<SecondaryDisplayLauncher> allAppsStore, OnLongClickListener onLongClickListener)69     public PinnedAppsAdapter(
70             SecondaryDisplayLauncher launcher,
71             AllAppsStore<SecondaryDisplayLauncher> allAppsStore,
72             OnLongClickListener onLongClickListener) {
73         mLauncher = launcher;
74         mOnClickListener = launcher.getItemOnClickListener();
75         mOnLongClickListener = onLongClickListener;
76         mAllAppsList = allAppsStore;
77         mPrefs = launcher.getSharedPreferences(PINNED_APPS_KEY, MODE_PRIVATE);
78         mAppNameComparator = new AppInfoComparator(launcher);
79 
80         mAllAppsList.addUpdateListener(this::createFilteredAppsList);
81     }
82 
83     /**
84      * {@inheritDoc}
85      */
86     @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)87     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
88         if (PINNED_APPS_KEY.equals(key)) {
89             Executors.MODEL_EXECUTOR.submit(() -> {
90                 Set<ComponentKey> apps = prefs.getStringSet(key, Collections.emptySet())
91                         .stream()
92                         .map(this::parseComponentKey)
93                         .filter(Objects::nonNull)
94                         .collect(Collectors.toSet());
95                 Executors.MAIN_EXECUTOR.submit(() -> {
96                     mPinnedApps.clear();
97                     mPinnedApps.addAll(apps);
98                     createFilteredAppsList();
99                 });
100             });
101         }
102     }
103 
104     /**
105      * {@inheritDoc}
106      */
107     @Override
getCount()108     public int getCount() {
109         return mItems.size();
110     }
111 
112     /**
113      * {@inheritDoc}
114      */
115     @Override
getItem(int position)116     public AppInfo getItem(int position) {
117         return mItems.get(position);
118     }
119 
120     /**
121      * {@inheritDoc}
122      */
123     @Override
getItemId(int position)124     public long getItemId(int position) {
125         return position;
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     @Override
getView(int position, View view, ViewGroup parent)132     public View getView(int position, View view, ViewGroup parent) {
133         BubbleTextView icon;
134         if (view instanceof BubbleTextView) {
135             icon = (BubbleTextView) view;
136         } else {
137             icon = (BubbleTextView) LayoutInflater.from(parent.getContext())
138                     .inflate(R.layout.app_icon, parent, false);
139             icon.setOnClickListener(mOnClickListener);
140             icon.setOnLongClickListener(mOnLongClickListener);
141             icon.setLongPressTimeoutFactor(1f);
142             int padding = mLauncher.getDeviceProfile().edgeMarginPx;
143             icon.setPadding(padding, padding, padding, padding);
144         }
145 
146         icon.applyFromApplicationInfo(mItems.get(position));
147         return icon;
148     }
149 
createFilteredAppsList()150     private void createFilteredAppsList() {
151         mItems.clear();
152         mPinnedApps.stream().map(mAllAppsList::getApp)
153                 .filter(Objects::nonNull).forEach(mItems::add);
154         mItems.sort(mAppNameComparator);
155         notifyDataSetChanged();
156     }
157 
158     /**
159      * Initialized the pinned apps list and starts listening for changes
160      */
init()161     public void init() {
162         mPrefs.registerOnSharedPreferenceChangeListener(this);
163         onSharedPreferenceChanged(mPrefs, PINNED_APPS_KEY);
164     }
165 
166     /**
167      * Stops listening for any pinned apps changes
168      */
destroy()169     public void destroy() {
170         mPrefs.unregisterOnSharedPreferenceChangeListener(this);
171     }
172 
173     /**
174      * Pins or unpins apps from home screen
175      */
update(ItemInfo info, Function<ComponentKey, Boolean> op)176     public void update(ItemInfo info, Function<ComponentKey, Boolean> op) {
177         ComponentKey key = new ComponentKey(info.getTargetComponent(), info.user);
178         if (op.apply(key)) {
179             createFilteredAppsList();
180             Set<ComponentKey> copy = new HashSet<>(mPinnedApps);
181             Executors.MODEL_EXECUTOR.submit(() ->
182                     mPrefs.edit().putStringSet(PINNED_APPS_KEY,
183                                     copy.stream().map(this::encode).collect(Collectors.toSet()))
184                             .apply());
185         }
186     }
187 
parseComponentKey(String string)188     private ComponentKey parseComponentKey(String string) {
189         try {
190             String[] parts = string.split("#");
191             UserHandle user;
192             if (parts.length > 2) {
193                 user = UserCache.INSTANCE.get(mLauncher)
194                         .getUserForSerialNumber(Long.parseLong(parts[2]));
195             } else {
196                 user = Process.myUserHandle();
197             }
198             ComponentName cn = ComponentName.unflattenFromString(parts[0]);
199             return new ComponentKey(cn, user);
200         } catch (Exception e) {
201             return null;
202         }
203     }
204 
encode(ComponentKey key)205     private String encode(ComponentKey key) {
206         return key.componentName.flattenToShortString() + "#"
207                 + UserCache.INSTANCE.get(mLauncher).getSerialNumberForUser(key.user);
208     }
209 
210     /**
211      * Returns a system shortcut to pin/unpin a shortcut
212      */
getSystemShortcut(ItemInfo info, View originalView)213     public SystemShortcut getSystemShortcut(ItemInfo info, View originalView) {
214         return new PinUnPinShortcut(mLauncher, info, originalView,
215                 mPinnedApps.contains(new ComponentKey(info.getTargetComponent(), info.user)));
216     }
217 
218     /**
219      * Pins app to home screen
220      */
addPinnedApp(ItemInfo info)221     public void addPinnedApp(ItemInfo info) {
222         update(info, mPinnedApps::add);
223     }
224 
225     private class PinUnPinShortcut extends SystemShortcut<SecondaryDisplayLauncher> {
226 
227         private final boolean mIsPinned;
228 
PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, View originalView, boolean isPinned)229         PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, View originalView,
230                 boolean isPinned) {
231             super(isPinned ? R.drawable.ic_remove_no_shadow : R.drawable.ic_pin,
232                     isPinned ? R.string.remove_drop_target_label : R.string.action_add_to_workspace,
233                     target, info, originalView);
234             mIsPinned = isPinned;
235         }
236 
237         @Override
onClick(View view)238         public void onClick(View view) {
239             if (mIsPinned) {
240                 update(mItemInfo, mPinnedApps::remove);
241             } else {
242                 update(mItemInfo, mPinnedApps::add);
243             }
244             AbstractFloatingView.closeAllOpenViews(mLauncher);
245         }
246     }
247 }
248