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