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 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 allAppsStore, OnLongClickListener onLongClickListener)69     public PinnedAppsAdapter(SecondaryDisplayLauncher launcher, AllAppsStore allAppsStore,
70             OnLongClickListener onLongClickListener) {
71         mLauncher = launcher;
72         mOnClickListener = launcher.getItemOnClickListener();
73         mOnLongClickListener = onLongClickListener;
74         mAllAppsList = allAppsStore;
75         mPrefs = launcher.getSharedPreferences(PINNED_APPS_KEY, MODE_PRIVATE);
76         mAppNameComparator = new AppInfoComparator(launcher);
77 
78         mAllAppsList.addUpdateListener(this::createFilteredAppsList);
79     }
80 
81     /**
82      * {@inheritDoc}
83      */
84     @Override
onSharedPreferenceChanged(SharedPreferences prefs, String key)85     public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
86         if (PINNED_APPS_KEY.equals(key)) {
87             Executors.MODEL_EXECUTOR.submit(() -> {
88                 Set<ComponentKey> apps = prefs.getStringSet(key, Collections.emptySet())
89                         .stream()
90                         .map(this::parseComponentKey)
91                         .filter(Objects::nonNull)
92                         .collect(Collectors.toSet());
93                 Executors.MAIN_EXECUTOR.submit(() -> {
94                     mPinnedApps.clear();
95                     mPinnedApps.addAll(apps);
96                     createFilteredAppsList();
97                 });
98             });
99         }
100     }
101 
102     /**
103      * {@inheritDoc}
104      */
105     @Override
getCount()106     public int getCount() {
107         return mItems.size();
108     }
109 
110     /**
111      * {@inheritDoc}
112      */
113     @Override
getItem(int position)114     public AppInfo getItem(int position) {
115         return mItems.get(position);
116     }
117 
118     /**
119      * {@inheritDoc}
120      */
121     @Override
getItemId(int position)122     public long getItemId(int position) {
123         return position;
124     }
125 
126     /**
127      * {@inheritDoc}
128      */
129     @Override
getView(int position, View view, ViewGroup parent)130     public View getView(int position, View view, ViewGroup parent) {
131         BubbleTextView icon;
132         if (view instanceof BubbleTextView) {
133             icon = (BubbleTextView) view;
134         } else {
135             icon = (BubbleTextView) LayoutInflater.from(parent.getContext())
136                     .inflate(R.layout.app_icon, parent, false);
137             icon.setOnClickListener(mOnClickListener);
138             icon.setOnLongClickListener(mOnLongClickListener);
139             icon.setLongPressTimeoutFactor(1f);
140             int padding = mLauncher.getDeviceProfile().edgeMarginPx;
141             icon.setPadding(padding, padding, padding, padding);
142         }
143 
144         icon.applyFromApplicationInfo(mItems.get(position));
145         return icon;
146     }
147 
createFilteredAppsList()148     private void createFilteredAppsList() {
149         mItems.clear();
150         mPinnedApps.stream().map(mAllAppsList::getApp)
151                 .filter(Objects::nonNull).forEach(mItems::add);
152         mItems.sort(mAppNameComparator);
153         notifyDataSetChanged();
154     }
155 
156     /**
157      * Initialized the pinned apps list and starts listening for changes
158      */
init()159     public void init() {
160         mPrefs.registerOnSharedPreferenceChangeListener(this);
161         onSharedPreferenceChanged(mPrefs, PINNED_APPS_KEY);
162     }
163 
164     /**
165      * Stops listening for any pinned apps changes
166      */
destroy()167     public void destroy() {
168         mPrefs.unregisterOnSharedPreferenceChangeListener(this);
169     }
170 
update(ItemInfo info, Function<ComponentKey, Boolean> op)171     private void update(ItemInfo info, Function<ComponentKey, Boolean> op) {
172         ComponentKey key = new ComponentKey(info.getTargetComponent(), info.user);
173         if (op.apply(key)) {
174             createFilteredAppsList();
175             Set<ComponentKey> copy = new HashSet<>(mPinnedApps);
176             Executors.MODEL_EXECUTOR.submit(() ->
177                     mPrefs.edit().putStringSet(PINNED_APPS_KEY,
178                         copy.stream().map(this::encode).collect(Collectors.toSet()))
179                         .apply());
180         }
181     }
182 
parseComponentKey(String string)183     private ComponentKey parseComponentKey(String string) {
184         try {
185             String[] parts = string.split("#");
186             UserHandle user;
187             if (parts.length > 2) {
188                 user = UserCache.INSTANCE.get(mLauncher)
189                         .getUserForSerialNumber(Long.parseLong(parts[2]));
190             } else {
191                 user = Process.myUserHandle();
192             }
193             ComponentName cn = ComponentName.unflattenFromString(parts[0]);
194             return new ComponentKey(cn, user);
195         } catch (Exception e) {
196             return null;
197         }
198     }
199 
encode(ComponentKey key)200     private String encode(ComponentKey key) {
201         return key.componentName.flattenToShortString() + "#"
202                 + UserCache.INSTANCE.get(mLauncher).getSerialNumberForUser(key.user);
203     }
204 
205     /**
206      * Returns a system shortcut to pin/unpin a shortcut
207      */
getSystemShortcut(ItemInfo info)208     public SystemShortcut getSystemShortcut(ItemInfo info) {
209         return new PinUnPinShortcut(mLauncher, info,
210                 mPinnedApps.contains(new ComponentKey(info.getTargetComponent(), info.user)));
211     }
212 
213     private class PinUnPinShortcut extends SystemShortcut<SecondaryDisplayLauncher> {
214 
215         private final boolean mIsPinned;
216 
PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, boolean isPinned)217         PinUnPinShortcut(SecondaryDisplayLauncher target, ItemInfo info, boolean isPinned) {
218             super(isPinned ? R.drawable.ic_remove_no_shadow : R.drawable.ic_pin,
219                     isPinned ? R.string.remove_drop_target_label : R.string.action_add_to_workspace,
220                     target, info);
221             mIsPinned = isPinned;
222         }
223 
224         @Override
onClick(View view)225         public void onClick(View view) {
226             if (mIsPinned) {
227                 update(mItemInfo, mPinnedApps::remove);
228             } else {
229                 update(mItemInfo, mPinnedApps::add);
230             }
231             AbstractFloatingView.closeAllOpenViews(mLauncher);
232         }
233     }
234 }
235