1 /* 2 * Copyright (C) 2018 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 17 package com.example.android.intentplayground; 18 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.PackageInfo; 24 import android.content.pm.PackageManager; 25 import android.content.res.ColorStateList; 26 import android.util.Log; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.widget.CheckBox; 31 import android.widget.CompoundButton; 32 import android.widget.FrameLayout; 33 import android.widget.LinearLayout; 34 import android.widget.RadioButton; 35 import android.widget.RadioGroup; 36 import android.widget.TextView; 37 import android.widget.Toast; 38 39 import androidx.annotation.NonNull; 40 41 import java.util.ArrayList; 42 import java.util.Arrays; 43 import java.util.Collection; 44 import java.util.Comparator; 45 import java.util.HashMap; 46 import java.util.LinkedList; 47 import java.util.List; 48 import java.util.Map; 49 import java.util.stream.Collectors; 50 51 /** 52 * Displays options to build an intent with different configurations of flags 53 * and target activities, and allows the user to launch an activity with the built intent. 54 */ 55 public class IntentBuilderView extends FrameLayout implements View.OnClickListener, 56 CompoundButton.OnCheckedChangeListener { 57 private static final String TAG = "IntentBuilderView"; 58 protected final int TAG_FLAG = R.id.tag_flag; 59 protected final int TAG_SUGGESTED = R.id.tag_suggested; 60 protected ComponentName mActivityToLaunch; 61 private boolean mVerifyMode; 62 private ColorStateList mSuggestTint; 63 private ColorStateList mDefaultTint; 64 private LinearLayout mLayout; 65 private Context mContext; 66 private LayoutInflater mInflater; 67 private List<RadioButton> mRadioButtons; 68 69 /** 70 * Constructs a new IntentBuilderView, in the specified mode. 71 * 72 * @param context The context of the activity that holds this view. 73 * @param mode The mode to launch in (if null, default mode turns suggestions off). Passing 74 * {@link BaseActivity.Mode} will turn on suggestions 75 * by default. 76 */ IntentBuilderView(@onNull Context context, BaseActivity.Mode mode)77 public IntentBuilderView(@NonNull Context context, BaseActivity.Mode mode) { 78 super(context); 79 mContext = context; 80 mInflater = LayoutInflater.from(context); 81 mLayout = (LinearLayout) mInflater.inflate(R.layout.view_build_intent, 82 this /* root */, false /* attachToRoot */); 83 addView(mLayout, new LayoutParams(LayoutParams.MATCH_PARENT, 84 LayoutParams.MATCH_PARENT)); 85 mActivityToLaunch = new ComponentName(context, 86 TaskAffinity1Activity.class); 87 mSuggestTint = context.getColorStateList(R.color.suggested_checkbox); 88 mDefaultTint = context.getColorStateList(R.color.default_checkbox); 89 mVerifyMode = mode != null && mode == BaseActivity.Mode.VERIFY; 90 setTag(BaseActivity.BUILDER_VIEW); 91 setId(R.id.build_intent_container); 92 setBackground(context.getResources().getDrawable(R.drawable.card_background, 93 null /*theme*/)); 94 setupViews(); 95 } 96 getClass(String name)97 private Class<?> getClass(String name) { 98 String fullName = mContext.getPackageName().concat(".").concat(name); 99 try { 100 return Class.forName(fullName); 101 } catch (ClassNotFoundException e) { 102 if (BuildConfig.DEBUG) e.printStackTrace(); 103 throw new RuntimeException(e); 104 } 105 } 106 setupViews()107 private void setupViews() { 108 PackageInfo packInfo; 109 110 // Retrieve activities and their manifest flags 111 PackageManager pm = mContext.getPackageManager(); 112 try { 113 packInfo = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES); 114 } catch (PackageManager.NameNotFoundException e) { 115 Toast.makeText(mContext, 116 "Cannot find activities, this should never happen " + e.toString(), 117 Toast.LENGTH_SHORT).show(); 118 throw new RuntimeException(e); 119 } 120 List<ActivityInfo> activities = Arrays.asList(packInfo.activities); 121 Map<ActivityInfo, List<String>> activityToFlags = new HashMap<>(); 122 activities.forEach(activityInfo -> 123 activityToFlags.put(activityInfo, FlagUtils.getActivityFlags(activityInfo))); 124 125 // Get handles to views 126 LinearLayout flagBuilderLayout = mLayout.findViewById(R.id.build_intent_flags); 127 RadioGroup activityRadios = mLayout.findViewById(R.id.radioGroup_launchMode); 128 // Populate views with text 129 fillCheckBoxLayout(flagBuilderLayout, FlagUtils.intentFlagsByCategory(), 130 R.layout.section_header, R.id.header_title, R.layout.checkbox_list_item, 131 R.id.checkBox_item); 132 133 // Add radios for activity combos 134 List<RadioButton> radioButtons = new ArrayList<>(); 135 activityToFlags.entrySet().stream() 136 .sorted(Comparator.comparing( 137 activityEntry -> nameOfActivityInfo(activityEntry.getKey()))) 138 .forEach(activityEntry -> { 139 ActivityInfo activityInfo = activityEntry.getKey(); 140 List<String> manifestFlags = activityEntry.getValue(); 141 142 LinearLayout actRadio = (LinearLayout) mInflater 143 .inflate(R.layout.activity_radio_list_item, null /* root */); 144 RadioButton rb = actRadio.findViewById(R.id.radio_launchMode); 145 rb.setText(activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1)); 146 rb.setTag(activityInfo); 147 ((TextView) actRadio.findViewById(R.id.activity_desc)).setText( 148 manifestFlags.stream().collect(Collectors.joining("\n"))); 149 rb.setOnClickListener(this); 150 activityRadios.addView(actRadio); 151 radioButtons.add(rb); 152 }); 153 ((CompoundButton) mLayout.findViewById(R.id.suggestion_switch)) 154 .setOnCheckedChangeListener(this); 155 mRadioButtons = radioButtons; 156 } 157 158 nameOfActivityInfo(ActivityInfo activityInfo)159 private String nameOfActivityInfo(ActivityInfo activityInfo) { 160 return activityInfo.name.substring(activityInfo.name.lastIndexOf('.') + 1); 161 } 162 163 /** 164 * Fills the {@link ViewGroup} with a list separated by section 165 * 166 * @param layout The layout to fill 167 * @param categories A map of category names to list items within that category 168 * @param categoryLayoutRes the layout resource of the category header view 169 * @param categoryViewId the resource id of the category {@link TextView} within the layout 170 * @param itemLayoutRes the layout resource of the list item view 171 * @param itemViewId the resource id of the item {@link TextView} within the item layout 172 */ fillCheckBoxLayout(ViewGroup layout, Map<String, List<String>> categories, int categoryLayoutRes, int categoryViewId, int itemLayoutRes, int itemViewId)173 private void fillCheckBoxLayout(ViewGroup layout, Map<String, List<String>> categories, 174 int categoryLayoutRes, int categoryViewId, int itemLayoutRes, int itemViewId) { 175 layout.removeAllViews(); 176 for (String category : categories.keySet()) { 177 View categoryLayout = mInflater.inflate(categoryLayoutRes, layout, 178 false /* attachToRoot */); 179 TextView categoryView = categoryLayout.findViewById(categoryViewId); 180 categoryView.setText(category); 181 layout.addView(categoryLayout); 182 for (String item : categories.get(category)) { 183 View itemLayout = mInflater.inflate(itemLayoutRes, layout, 184 false /* attachToRoot */); 185 CheckBox itemView = itemLayout.findViewById(itemViewId); 186 IntentFlag flag = FlagUtils.getFlagForString(item); 187 itemView.setTag(TAG_FLAG, flag); 188 itemView.setText(item); 189 itemView.setOnCheckedChangeListener(this); 190 layout.addView(itemLayout); 191 } 192 } 193 } 194 195 @Override onClick(View view)196 public void onClick(View view) { 197 // Handles selection of target activity 198 if (view instanceof RadioButton) { 199 ActivityInfo tag = (ActivityInfo) view.getTag(); 200 mActivityToLaunch = new ComponentName(mContext, 201 getClass(tag.name.substring(tag.name.lastIndexOf(".") + 1))); 202 mRadioButtons.stream().filter(rb -> rb != view) 203 .forEach(rb -> rb.setChecked(false)); 204 } 205 } 206 currentIntent()207 public Intent currentIntent() { 208 LinearLayout flagBuilder = mLayout.findViewById(R.id.build_intent_flags); 209 Intent intent = new Intent(); 210 // Gather flags from flag builder checkbox list 211 childrenOfGroup(flagBuilder, CheckBox.class) 212 .forEach(checkbox -> { 213 int flagVal = FlagUtils.flagValue(checkbox.getText().toString()); 214 if (checkbox.isChecked()) { 215 intent.addFlags(flagVal); 216 } else { 217 intent.removeFlags(flagVal); 218 } 219 }); 220 intent.setComponent(mActivityToLaunch); 221 return intent; 222 } 223 224 startForResult()225 public boolean startForResult() { 226 RadioButton startNormal = mLayout.findViewById(R.id.start_normal); 227 return !startNormal.isChecked(); 228 } 229 230 @Override onCheckedChanged(CompoundButton compoundButton, boolean checked)231 public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { 232 int buttonId = compoundButton.getId(); 233 if (buttonId == R.id.checkBox_item) { 234 // A checkbox was checked/unchecked 235 IntentFlag flag = (IntentFlag) compoundButton.getTag(TAG_FLAG); 236 if (flag != null && mVerifyMode) { 237 refreshConstraints(); 238 if (checked) { 239 suggestFlags(flag); 240 selectFlags(flag.getRequests()); 241 } else { 242 clearSuggestions(); 243 } 244 } 245 } else if (buttonId == R.id.suggestion_switch) { 246 // Suggestions were turned on/off 247 clearSuggestions(); 248 mVerifyMode = checked; 249 if (mVerifyMode) { 250 refreshConstraints(); 251 getCheckedFlags().forEach(this::suggestFlags); 252 } else { 253 enableAllFlags(); 254 } 255 } 256 } 257 refreshConstraints()258 private void refreshConstraints() { 259 enableAllFlags(); 260 getCheckedFlags().forEach(flag -> disableFlags(flag.getConflicts())); 261 } 262 suggestFlags(IntentFlag flag)263 private void suggestFlags(IntentFlag flag) { 264 clearSuggestions(); 265 List<String> suggestions = flag.getComplements().stream().map(IntentFlag::getName) 266 .collect(Collectors.toList()); 267 getAllCheckBoxes().stream() 268 .filter(box -> hasSuggestion(suggestions, box)) 269 .forEach(box -> { 270 box.setButtonTintList(mSuggestTint); 271 box.setTag(TAG_SUGGESTED, true); 272 }); 273 } 274 hasSuggestion(List<String> suggestions, CheckBox box)275 private boolean hasSuggestion(List<String> suggestions, CheckBox box) { 276 IntentFlag flag = (IntentFlag) box.getTag(TAG_FLAG); 277 if (flag != null) { 278 return suggestions.contains(flag.getName()); 279 } else { 280 Log.w(TAG, "Unknown flag: " + box.getText()); 281 return false; 282 } 283 } 284 clearSuggestions()285 private void clearSuggestions() { 286 getAllCheckBoxes().forEach(box -> box.setButtonTintList(mDefaultTint)); 287 } 288 289 /** 290 * Clears all of the checkboxes in this builder. 291 */ clearFlags()292 public void clearFlags() { 293 getAllCheckBoxes().forEach(box -> box.setChecked(false)); 294 } 295 getAllCheckBoxes()296 private List<CheckBox> getAllCheckBoxes() { 297 View layout = mLayout; 298 ViewGroup flagBuilder = (LinearLayout) layout.findViewById(R.id.build_intent_flags); 299 List<CheckBox> checkBoxes = new LinkedList<>(); 300 for (int i = 0; i < flagBuilder.getChildCount(); i++) { 301 View child = flagBuilder.getChildAt(i); 302 if (child instanceof CheckBox) { 303 checkBoxes.add((CheckBox) child); 304 } 305 } 306 return checkBoxes; 307 } 308 309 /** 310 * Retrieve children of a certain type from a {@link ViewGroup}. 311 * 312 * @param group the ViewGroup to retrieve children from. 313 */ childrenOfGroup(ViewGroup group, Class<T> viewType)314 protected static <T> List<T> childrenOfGroup(ViewGroup group, Class<T> viewType) { 315 List<T> list = new LinkedList<>(); 316 for (int i = 0; i < group.getChildCount(); i++) { 317 View v = group.getChildAt(i); 318 if (viewType.isAssignableFrom(v.getClass())) list.add(viewType.cast(v)); 319 } 320 return list; 321 } 322 323 /** 324 * Selects the checkboxes for the given list of flags. 325 * 326 * @param flags A list of mIntent flags to select. 327 */ selectFlags(List<String> flags)328 public void selectFlags(List<String> flags) { 329 getAllCheckBoxes().forEach(box -> { 330 if (flags.contains(box.getText())) { 331 box.setChecked(true); 332 } 333 }); 334 } 335 336 /** 337 * Selects the checkboxes for the given list of flags. 338 * 339 * @param flags A list of mIntent flags to select. 340 */ selectFlags(Collection<IntentFlag> flags)341 public void selectFlags(Collection<IntentFlag> flags) { 342 selectFlags(flags.stream().map(IntentFlag::getName).collect(Collectors.toList())); 343 } 344 enableAllFlags()345 private void enableAllFlags() { 346 getAllCheckBoxes().forEach(box -> box.setEnabled(true)); 347 } 348 getChecked()349 private Collection<CheckBox> getChecked() { 350 return getAllCheckBoxes().stream().filter(CompoundButton::isChecked) 351 .collect(Collectors.toList()); 352 } 353 getCheckedFlags()354 private Collection<IntentFlag> getCheckedFlags() { 355 return getChecked().stream().map(checkBox -> (IntentFlag) checkBox.getTag(TAG_FLAG)) 356 .collect(Collectors.toList()); 357 } 358 disableFlags(Collection<IntentFlag> flags)359 private void disableFlags(Collection<IntentFlag> flags) { 360 flags.forEach(flag -> getCheckBox(flag).setEnabled(false)); 361 } 362 getCheckBox(IntentFlag flag)363 private CheckBox getCheckBox(IntentFlag flag) { 364 return getAllCheckBoxes().stream().filter(box -> flag.getName().equals(box.getText())) 365 .findFirst().orElse(null); 366 } 367 368 /** 369 * A functional interface that represents the action to take upon the user pressing the launch 370 * button within this view. 371 */ 372 public interface OnLaunchCallback { launchActivity(Intent intent, boolean forResult)373 void launchActivity(Intent intent, boolean forResult); 374 } 375 } 376