1 /*
2  * Copyright (C) 2017 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.systemui.tuner;
16 
17 import android.app.AlertDialog;
18 import android.app.AlertDialog.Builder;
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.ActivityInfo;
23 import android.content.pm.LauncherActivityInfo;
24 import android.content.pm.PackageManager.NameNotFoundException;
25 import android.graphics.drawable.Drawable;
26 import android.os.Bundle;
27 import android.os.Handler;
28 import android.text.TextUtils;
29 import android.util.TypedValue;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.widget.ImageView;
34 import android.widget.TextView;
35 
36 import androidx.preference.Preference;
37 import androidx.preference.PreferenceFragment;
38 import androidx.preference.SwitchPreference;
39 import androidx.recyclerview.widget.LinearLayoutManager;
40 import androidx.recyclerview.widget.RecyclerView;
41 import androidx.recyclerview.widget.RecyclerView.ViewHolder;
42 
43 import com.android.systemui.Dependency;
44 import com.android.systemui.plugins.IntentButtonProvider.IntentButton;
45 import com.android.systemui.res.R;
46 import com.android.systemui.statusbar.ScalingDrawableWrapper;
47 import com.android.systemui.statusbar.phone.ExpandableIndicator;
48 import com.android.systemui.statusbar.policy.ExtensionController.TunerFactory;
49 import com.android.systemui.tuner.ShortcutParser.Shortcut;
50 import com.android.systemui.tuner.TunerService.Tunable;
51 import com.android.tools.r8.keepanno.annotations.KeepTarget;
52 import com.android.tools.r8.keepanno.annotations.UsesReflection;
53 
54 import java.util.ArrayList;
55 import java.util.Map;
56 import java.util.function.Consumer;
57 
58 public class LockscreenFragment extends PreferenceFragment {
59 
60     private static final String KEY_LEFT = "left";
61     private static final String KEY_RIGHT = "right";
62     private static final String KEY_CUSTOMIZE = "customize";
63     private static final String KEY_SHORTCUT = "shortcut";
64 
65     public static final String LOCKSCREEN_LEFT_BUTTON = "sysui_keyguard_left";
66     public static final String LOCKSCREEN_LEFT_UNLOCK = "sysui_keyguard_left_unlock";
67     public static final String LOCKSCREEN_RIGHT_BUTTON = "sysui_keyguard_right";
68     public static final String LOCKSCREEN_RIGHT_UNLOCK = "sysui_keyguard_right_unlock";
69 
70     private final ArrayList<Tunable> mTunables = new ArrayList<>();
71     private TunerService mTunerService;
72     private Handler mHandler;
73 
74     // aapt doesn't generate keep rules for android:fragment references in <Preference> tags, so
75     // explicitly declare references per usage in `R.xml.lockscreen_settings`. See b/120445169.
76     @UsesReflection(@KeepTarget(classConstant = ShortcutPicker.class))
77     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)78     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
79         mTunerService = Dependency.get(TunerService.class);
80         mHandler = new Handler();
81         addPreferencesFromResource(R.xml.lockscreen_settings);
82         setupGroup(LOCKSCREEN_LEFT_BUTTON, LOCKSCREEN_LEFT_UNLOCK);
83         setupGroup(LOCKSCREEN_RIGHT_BUTTON, LOCKSCREEN_RIGHT_UNLOCK);
84     }
85 
86     @Override
onDestroy()87     public void onDestroy() {
88         super.onDestroy();
89         mTunables.forEach(t -> mTunerService.removeTunable(t));
90     }
91 
setupGroup(String buttonSetting, String unlockKey)92     private void setupGroup(String buttonSetting, String unlockKey) {
93         Preference shortcut = findPreference(buttonSetting);
94         SwitchPreference unlock = (SwitchPreference) findPreference(unlockKey);
95         addTunable((k, v) -> {
96             boolean visible = !TextUtils.isEmpty(v);
97             unlock.setVisible(visible);
98             setSummary(shortcut, v);
99         }, buttonSetting);
100     }
101 
showSelectDialog(String buttonSetting)102     private void showSelectDialog(String buttonSetting) {
103         RecyclerView v = (RecyclerView) LayoutInflater.from(getContext())
104                 .inflate(R.layout.tuner_shortcut_list, null);
105         v.setLayoutManager(new LinearLayoutManager(getContext()));
106         AlertDialog dialog = new Builder(getContext())
107                 .setView(v)
108                 .show();
109         Adapter adapter = new Adapter(getContext(), item -> {
110             mTunerService.setValue(buttonSetting, item.getSettingValue());
111             dialog.dismiss();
112         });
113 
114         v.setAdapter(adapter);
115     }
116 
setSummary(Preference shortcut, String value)117     private void setSummary(Preference shortcut, String value) {
118         if (value == null) {
119             shortcut.setSummary(R.string.lockscreen_none);
120             return;
121         }
122         if (value.contains("::")) {
123             Shortcut info = getShortcutInfo(getContext(), value);
124             shortcut.setSummary(info != null ? info.label : null);
125         } else if (value.contains("/")) {
126             ActivityInfo info = getActivityinfo(getContext(), value);
127             shortcut.setSummary(info != null ? info.loadLabel(getContext().getPackageManager())
128                     : null);
129         } else {
130             shortcut.setSummary(R.string.lockscreen_none);
131         }
132     }
133 
addTunable(Tunable t, String... keys)134     private void addTunable(Tunable t, String... keys) {
135         mTunables.add(t);
136         mTunerService.addTunable(t, keys);
137     }
138 
getActivityinfo(Context context, String value)139     public static ActivityInfo getActivityinfo(Context context, String value) {
140         ComponentName component = ComponentName.unflattenFromString(value);
141         try {
142             return context.getPackageManager().getActivityInfo(component, 0);
143         } catch (NameNotFoundException e) {
144             return null;
145         }
146     }
147 
getShortcutInfo(Context context, String value)148     public static Shortcut getShortcutInfo(Context context, String value) {
149         return Shortcut.create(context, value);
150     }
151 
152     public static class Holder extends ViewHolder {
153         public final ImageView icon;
154         public final TextView title;
155         public final ExpandableIndicator expand;
156 
Holder(View itemView)157         public Holder(View itemView) {
158             super(itemView);
159             icon = (ImageView) itemView.findViewById(android.R.id.icon);
160             title = (TextView) itemView.findViewById(android.R.id.title);
161             expand = (ExpandableIndicator) itemView.findViewById(R.id.expand);
162         }
163     }
164 
165     private static class StaticShortcut extends Item {
166 
167         private final Context mContext;
168         private final Shortcut mShortcut;
169 
170 
StaticShortcut(Context context, Shortcut shortcut)171         public StaticShortcut(Context context, Shortcut shortcut) {
172             mContext = context;
173             mShortcut = shortcut;
174         }
175 
176         @Override
getDrawable()177         public Drawable getDrawable() {
178             return mShortcut.icon.loadDrawable(mContext);
179         }
180 
181         @Override
getLabel()182         public String getLabel() {
183             return mShortcut.label;
184         }
185 
186         @Override
getSettingValue()187         public String getSettingValue() {
188             return mShortcut.toString();
189         }
190 
191         @Override
getExpando()192         public Boolean getExpando() {
193             return null;
194         }
195     }
196 
197     private static class App extends Item {
198 
199         private final Context mContext;
200         private final LauncherActivityInfo mInfo;
201         private final ArrayList<Item> mChildren = new ArrayList<>();
202         private boolean mExpanded;
203 
App(Context context, LauncherActivityInfo info)204         public App(Context context, LauncherActivityInfo info) {
205             mContext = context;
206             mInfo = info;
207             mExpanded = false;
208         }
209 
addChild(Item child)210         public void addChild(Item child) {
211             mChildren.add(child);
212         }
213 
214         @Override
getDrawable()215         public Drawable getDrawable() {
216             return mInfo.getBadgedIcon(mContext.getResources().getConfiguration().densityDpi);
217         }
218 
219         @Override
getLabel()220         public String getLabel() {
221             return mInfo.getLabel().toString();
222         }
223 
224         @Override
getSettingValue()225         public String getSettingValue() {
226             return mInfo.getComponentName().flattenToString();
227         }
228 
229         @Override
getExpando()230         public Boolean getExpando() {
231             return mChildren.size() != 0 ? mExpanded : null;
232         }
233 
234         @Override
toggleExpando(Adapter adapter)235         public void toggleExpando(Adapter adapter) {
236             mExpanded = !mExpanded;
237             if (mExpanded) {
238                 mChildren.forEach(child -> adapter.addItem(this, child));
239             } else {
240                 mChildren.forEach(child -> adapter.remItem(child));
241             }
242         }
243     }
244 
245     private abstract static class Item {
getDrawable()246         public abstract Drawable getDrawable();
247 
getLabel()248         public abstract String getLabel();
249 
getSettingValue()250         public abstract String getSettingValue();
251 
getExpando()252         public abstract Boolean getExpando();
253 
toggleExpando(Adapter adapter)254         public void toggleExpando(Adapter adapter) {
255         }
256     }
257 
258     public static class Adapter extends RecyclerView.Adapter<Holder> {
259         private ArrayList<Item> mItems = new ArrayList<>();
260         private final Context mContext;
261         private final Consumer<Item> mCallback;
262 
Adapter(Context context, Consumer<Item> callback)263         public Adapter(Context context, Consumer<Item> callback) {
264             mContext = context;
265             mCallback = callback;
266         }
267 
268         @Override
onCreateViewHolder(ViewGroup parent, int viewType)269         public Holder onCreateViewHolder(ViewGroup parent, int viewType) {
270             return new Holder(LayoutInflater.from(parent.getContext())
271                     .inflate(R.layout.tuner_shortcut_item, parent, false));
272         }
273 
274         @Override
onBindViewHolder(Holder holder, int position)275         public void onBindViewHolder(Holder holder, int position) {
276             Item item = mItems.get(position);
277             holder.icon.setImageDrawable(item.getDrawable());
278             holder.title.setText(item.getLabel());
279             holder.itemView.setOnClickListener(
280                     v -> mCallback.accept(mItems.get(holder.getAdapterPosition())));
281             Boolean expando = item.getExpando();
282             if (expando != null) {
283                 holder.expand.setVisibility(View.VISIBLE);
284                 holder.expand.setExpanded(expando);
285                 holder.expand.setOnClickListener(
286                         v -> mItems.get(holder.getAdapterPosition()).toggleExpando(Adapter.this));
287             } else {
288                 holder.expand.setVisibility(View.GONE);
289             }
290         }
291 
292         @Override
getItemCount()293         public int getItemCount() {
294             return mItems.size();
295         }
296 
addItem(Item item)297         public void addItem(Item item) {
298             mItems.add(item);
299             notifyDataSetChanged();
300         }
301 
remItem(Item item)302         public void remItem(Item item) {
303             int index = mItems.indexOf(item);
304             mItems.remove(item);
305             notifyItemRemoved(index);
306         }
307 
addItem(Item parent, Item child)308         public void addItem(Item parent, Item child) {
309             int index = mItems.indexOf(parent);
310             mItems.add(index + 1, child);
311             notifyItemInserted(index + 1);
312         }
313     }
314 
315     public static class LockButtonFactory implements TunerFactory<IntentButton> {
316 
317         private final String mKey;
318         private final Context mContext;
319 
LockButtonFactory(Context context, String key)320         public LockButtonFactory(Context context, String key) {
321             mContext = context;
322             mKey = key;
323         }
324 
325         @Override
keys()326         public String[] keys() {
327             return new String[]{mKey};
328         }
329 
330         @Override
create(Map<String, String> settings)331         public IntentButton create(Map<String, String> settings) {
332             String buttonStr = settings.get(mKey);
333             if (!TextUtils.isEmpty(buttonStr)) {
334                 if (buttonStr.contains("::")) {
335                     Shortcut shortcut = getShortcutInfo(mContext, buttonStr);
336                     if (shortcut != null) {
337                         return new ShortcutButton(mContext, shortcut);
338                     }
339                 } else if (buttonStr.contains("/")) {
340                     ActivityInfo info = getActivityinfo(mContext, buttonStr);
341                     if (info != null) {
342                         return new ActivityButton(mContext, info);
343                     }
344                 }
345             }
346             return null;
347         }
348     }
349 
350     private static class ShortcutButton implements IntentButton {
351         private final Shortcut mShortcut;
352         private final IconState mIconState;
353 
ShortcutButton(Context context, Shortcut shortcut)354         public ShortcutButton(Context context, Shortcut shortcut) {
355             mShortcut = shortcut;
356             mIconState = new IconState();
357             mIconState.isVisible = true;
358             mIconState.drawable = shortcut.icon.loadDrawable(context).mutate();
359             mIconState.contentDescription = mShortcut.label;
360             int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32,
361                     context.getResources().getDisplayMetrics());
362             mIconState.drawable = new ScalingDrawableWrapper(mIconState.drawable,
363                     size / (float) mIconState.drawable.getIntrinsicWidth());
364             mIconState.tint = false;
365         }
366 
367         @Override
getIcon()368         public IconState getIcon() {
369             return mIconState;
370         }
371 
372         @Override
getIntent()373         public Intent getIntent() {
374             return mShortcut.intent;
375         }
376     }
377 
378     private static class ActivityButton implements IntentButton {
379         private final Intent mIntent;
380         private final IconState mIconState;
381 
ActivityButton(Context context, ActivityInfo info)382         public ActivityButton(Context context, ActivityInfo info) {
383             mIntent = new Intent().setComponent(new ComponentName(info.packageName, info.name));
384             mIconState = new IconState();
385             mIconState.isVisible = true;
386             mIconState.drawable = info.loadIcon(context.getPackageManager()).mutate();
387             mIconState.contentDescription = info.loadLabel(context.getPackageManager());
388             int size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 32,
389                     context.getResources().getDisplayMetrics());
390             mIconState.drawable = new ScalingDrawableWrapper(mIconState.drawable,
391                     size / (float) mIconState.drawable.getIntrinsicWidth());
392             mIconState.tint = false;
393         }
394 
395         @Override
getIcon()396         public IconState getIcon() {
397             return mIconState;
398         }
399 
400         @Override
getIntent()401         public Intent getIntent() {
402             return mIntent;
403         }
404     }
405 }
406