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