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