1 /*
2  * Copyright (C) 2014 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.apprestrictionenforcer;
18 
19 import android.app.Activity;
20 import android.app.admin.DevicePolicyManager;
21 import android.content.Context;
22 import android.content.RestrictionEntry;
23 import android.content.RestrictionsManager;
24 import android.content.SharedPreferences;
25 import android.os.Build;
26 import android.os.Bundle;
27 import android.os.Parcelable;
28 import android.support.annotation.NonNull;
29 import android.support.annotation.Nullable;
30 import android.support.v4.app.Fragment;
31 import android.text.Editable;
32 import android.text.TextUtils;
33 import android.text.TextWatcher;
34 import android.view.LayoutInflater;
35 import android.view.View;
36 import android.view.ViewGroup;
37 import android.widget.AdapterView;
38 import android.widget.ArrayAdapter;
39 import android.widget.Button;
40 import android.widget.CompoundButton;
41 import android.widget.EditText;
42 import android.widget.LinearLayout;
43 import android.widget.Spinner;
44 import android.widget.Switch;
45 import android.widget.TextView;
46 import android.widget.Toast;
47 
48 import java.util.ArrayList;
49 import java.util.Arrays;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Map;
53 
54 /**
55  * This fragment provides UI and functionality to set restrictions on the AppRestrictionSchema
56  * sample.
57  */
58 public class AppRestrictionEnforcerFragment extends Fragment implements
59         CompoundButton.OnCheckedChangeListener, AdapterView.OnItemSelectedListener,
60         View.OnClickListener, ItemAddFragment.OnItemAddedListener {
61 
62     /**
63      * Key for {@link SharedPreferences}
64      */
65     private static final String PREFS_KEY = "AppRestrictionEnforcerFragment";
66 
67     /**
68      * Key for the boolean restriction in AppRestrictionSchema.
69      */
70     private static final String RESTRICTION_KEY_SAY_HELLO = "can_say_hello";
71 
72     /**
73      * Key for the string restriction in AppRestrictionSchema.
74      */
75     private static final String RESTRICTION_KEY_MESSAGE = "message";
76 
77     /**
78      * Key for the integer restriction in AppRestrictionSchema.
79      */
80     private static final String RESTRICTION_KEY_NUMBER = "number";
81 
82     /**
83      * Key for the choice restriction in AppRestrictionSchema.
84      */
85     private static final String RESTRICTION_KEY_RANK = "rank";
86 
87     /**
88      * Key for the multi-select restriction in AppRestrictionSchema.
89      */
90     private static final String RESTRICTION_KEY_APPROVALS = "approvals";
91 
92     /**
93      * Key for the bundle array restriction in AppRestrictionSchema.
94      */
95     private static final String RESTRICTION_KEY_ITEMS = "items";
96     private static final String RESTRICTION_KEY_ITEM_KEY = "key";
97     private static final String RESTRICTION_KEY_ITEM_VALUE = "value";
98 
99     private static final String DELIMETER = ",";
100     private static final String SEPARATOR = ":";
101 
102     private static final boolean BUNDLE_SUPPORTED = Build.VERSION.SDK_INT >= 23;
103 
104     /**
105      * Current status of the restrictions.
106      */
107     private Bundle mCurrentRestrictions = new Bundle();
108 
109     // UI Components
110     private Switch mSwitchSayHello;
111     private EditText mEditMessage;
112     private EditText mEditNumber;
113     private Spinner mSpinnerRank;
114     private LinearLayout mLayoutApprovals;
115     private LinearLayout mLayoutItems;
116 
117     @Override
onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)118     public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
119                              @Nullable Bundle savedInstanceState) {
120         return inflater.inflate(R.layout.fragment_app_restriction_enforcer, container, false);
121     }
122 
123     @Override
onViewCreated(View view, @Nullable Bundle savedInstanceState)124     public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
125         // Retain references for the UI elements
126         mSwitchSayHello = (Switch) view.findViewById(R.id.say_hello);
127         mEditMessage = (EditText) view.findViewById(R.id.message);
128         mEditNumber = (EditText) view.findViewById(R.id.number);
129         mSpinnerRank = (Spinner) view.findViewById(R.id.rank);
130         mLayoutApprovals = (LinearLayout) view.findViewById(R.id.approvals);
131         mLayoutItems = (LinearLayout) view.findViewById(R.id.items);
132         view.findViewById(R.id.item_add).setOnClickListener(this);
133         View bundleArrayLayout = view.findViewById(R.id.bundle_array_layout);
134         if (BUNDLE_SUPPORTED) {
135             bundleArrayLayout.setVisibility(View.VISIBLE);
136         } else {
137             bundleArrayLayout.setVisibility(View.GONE);
138         }
139     }
140 
141     @Override
onResume()142     public void onResume() {
143         super.onResume();
144         loadRestrictions(getActivity());
145     }
146 
147     @Override
onCheckedChanged(CompoundButton compoundButton, boolean checked)148     public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
149         switch (compoundButton.getId()) {
150             case R.id.say_hello: {
151                 saveCanSayHello(getActivity(), checked);
152                 break;
153             }
154             case R.id.approval: {
155                 if (checked) {
156                     addApproval(getActivity(), (String) compoundButton.getTag());
157                 } else {
158                     removeApproval(getActivity(), (String) compoundButton.getTag());
159                 }
160                 break;
161             }
162         }
163     }
164 
165     private TextWatcher mWatcherMessage = new EasyTextWatcher() {
166         @Override
167         public void afterTextChanged(Editable s) {
168             saveMessage(getActivity(), s.toString());
169         }
170     };
171 
172     private TextWatcher mWatcherNumber = new EasyTextWatcher() {
173         @Override
174         public void afterTextChanged(Editable s) {
175             try {
176                 String string = s.toString();
177                 if (!TextUtils.isEmpty(string)) {
178                     saveNumber(getActivity(), Integer.parseInt(string));
179                 }
180             } catch (NumberFormatException e) {
181                 Toast.makeText(getActivity(), "Not an integer!", Toast.LENGTH_SHORT).show();
182             }
183         }
184     };
185 
186     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)187     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
188         switch (parent.getId()) {
189             case R.id.rank: {
190                 saveRank(getActivity(), (String) parent.getAdapter().getItem(position));
191                 break;
192             }
193         }
194     }
195 
196     @Override
onNothingSelected(AdapterView<?> parent)197     public void onNothingSelected(AdapterView<?> parent) {
198         // Nothing to do
199     }
200 
201     @Override
onClick(View v)202     public void onClick(View v) {
203         switch (v.getId()) {
204             case R.id.item_add:
205                 new ItemAddFragment().show(getChildFragmentManager(), "dialog");
206                 break;
207             case R.id.item_remove:
208                 String key = (String) v.getTag();
209                 removeItem(key);
210                 mLayoutItems.removeView((View) v.getParent());
211                 break;
212         }
213     }
214 
215     @Override
onItemAdded(String key, String value)216     public void onItemAdded(String key, String value) {
217         key = TextUtils.replace(key,
218                 new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
219         value = TextUtils.replace(value,
220                 new String[]{DELIMETER, SEPARATOR}, new String[]{"", ""}).toString();
221         Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
222         Map<String, String> items = new HashMap<>();
223         if (parcelables != null) {
224             for (Parcelable parcelable : parcelables) {
225                 Bundle bundle = (Bundle) parcelable;
226                 items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
227                         bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
228             }
229         }
230         items.put(key, value);
231         insertItemRow(LayoutInflater.from(getActivity()), key, value);
232         saveItems(getActivity(), items);
233     }
234 
235     /**
236      * Loads the restrictions for the AppRestrictionSchema sample.
237      *
238      * @param activity The activity
239      */
loadRestrictions(Activity activity)240     private void loadRestrictions(Activity activity) {
241         RestrictionsManager manager =
242                 (RestrictionsManager) activity.getSystemService(Context.RESTRICTIONS_SERVICE);
243         List<RestrictionEntry> restrictions =
244                 manager.getManifestRestrictions(Constants.PACKAGE_NAME_APP_RESTRICTION_SCHEMA);
245         SharedPreferences prefs = activity.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE);
246         for (RestrictionEntry restriction : restrictions) {
247             String key = restriction.getKey();
248             if (RESTRICTION_KEY_SAY_HELLO.equals(key)) {
249                 updateCanSayHello(prefs.getBoolean(RESTRICTION_KEY_SAY_HELLO,
250                         restriction.getSelectedState()));
251             } else if (RESTRICTION_KEY_MESSAGE.equals(key)) {
252                 updateMessage(prefs.getString(RESTRICTION_KEY_MESSAGE,
253                         restriction.getSelectedString()));
254             } else if (RESTRICTION_KEY_NUMBER.equals(key)) {
255                 updateNumber(prefs.getInt(RESTRICTION_KEY_NUMBER,
256                         restriction.getIntValue()));
257             } else if (RESTRICTION_KEY_RANK.equals(key)) {
258                 updateRank(activity, restriction.getChoiceValues(),
259                         prefs.getString(RESTRICTION_KEY_RANK, restriction.getSelectedString()));
260             } else if (RESTRICTION_KEY_APPROVALS.equals(key)) {
261                 updateApprovals(activity, restriction.getChoiceValues(),
262                         TextUtils.split(prefs.getString(RESTRICTION_KEY_APPROVALS,
263                                         TextUtils.join(DELIMETER,
264                                                 restriction.getAllSelectedStrings())),
265                                 DELIMETER));
266             } else if (BUNDLE_SUPPORTED && RESTRICTION_KEY_ITEMS.equals(key)) {
267                 String itemsString = prefs.getString(RESTRICTION_KEY_ITEMS, "");
268                 HashMap<String, String> items = new HashMap<>();
269                 for (String itemString : TextUtils.split(itemsString, DELIMETER)) {
270                     String[] strings = itemString.split(SEPARATOR, 2);
271                     items.put(strings[0], strings[1]);
272                 }
273                 updateItems(activity, items);
274             }
275         }
276     }
277 
updateCanSayHello(boolean canSayHello)278     private void updateCanSayHello(boolean canSayHello) {
279         mCurrentRestrictions.putBoolean(RESTRICTION_KEY_SAY_HELLO, canSayHello);
280         mSwitchSayHello.setOnCheckedChangeListener(null);
281         mSwitchSayHello.setChecked(canSayHello);
282         mSwitchSayHello.setOnCheckedChangeListener(this);
283     }
284 
updateMessage(String message)285     private void updateMessage(String message) {
286         mCurrentRestrictions.putString(RESTRICTION_KEY_MESSAGE, message);
287         mEditMessage.removeTextChangedListener(mWatcherMessage);
288         mEditMessage.setText(message);
289         mEditMessage.addTextChangedListener(mWatcherMessage);
290     }
291 
updateNumber(int number)292     private void updateNumber(int number) {
293         mCurrentRestrictions.putInt(RESTRICTION_KEY_NUMBER, number);
294         mEditNumber.removeTextChangedListener(mWatcherNumber);
295         mEditNumber.setText(String.valueOf(number));
296         mEditNumber.addTextChangedListener(mWatcherNumber);
297     }
298 
updateRank(Context context, String[] ranks, String selectedRank)299     private void updateRank(Context context, String[] ranks, String selectedRank) {
300         mCurrentRestrictions.putString(RESTRICTION_KEY_RANK, selectedRank);
301         mSpinnerRank.setAdapter(new ArrayAdapter<>(context,
302                 android.R.layout.simple_spinner_dropdown_item, ranks));
303         mSpinnerRank.setSelection(search(ranks, selectedRank));
304         mSpinnerRank.setOnItemSelectedListener(this);
305     }
306 
updateApprovals(Context context, String[] approvals, String[] selectedApprovals)307     private void updateApprovals(Context context, String[] approvals,
308                                  String[] selectedApprovals) {
309         mCurrentRestrictions.putStringArray(RESTRICTION_KEY_APPROVALS, selectedApprovals);
310         mLayoutApprovals.removeAllViews();
311         for (String approval : approvals) {
312             Switch sw = new Switch(context);
313             sw.setText(approval);
314             sw.setTag(approval);
315             sw.setChecked(Arrays.asList(selectedApprovals).contains(approval));
316             sw.setOnCheckedChangeListener(this);
317             sw.setId(R.id.approval);
318             mLayoutApprovals.addView(sw);
319         }
320     }
321 
updateItems(Context context, Map<String, String> items)322     private void updateItems(Context context, Map<String, String> items) {
323         if (!BUNDLE_SUPPORTED) {
324             return;
325         }
326         mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
327         LayoutInflater inflater = LayoutInflater.from(context);
328         mLayoutItems.removeAllViews();
329         for (String key : items.keySet()) {
330             insertItemRow(inflater, key, items.get(key));
331         }
332     }
333 
insertItemRow(LayoutInflater inflater, String key, String value)334     private void insertItemRow(LayoutInflater inflater, String key, String value) {
335         View view = inflater.inflate(R.layout.item, mLayoutItems, false);
336         TextView textView = (TextView) view.findViewById(R.id.item_text);
337         textView.setText(getString(R.string.item, key, value));
338         Button remove = (Button) view.findViewById(R.id.item_remove);
339         remove.setTag(key);
340         remove.setOnClickListener(this);
341         mLayoutItems.addView(view);
342     }
343 
344     @NonNull
convertToBundles(Map<String, String> items)345     private Bundle[] convertToBundles(Map<String, String> items) {
346         Bundle[] bundles = new Bundle[items.size()];
347         int i = 0;
348         for (String key : items.keySet()) {
349             Bundle bundle = new Bundle();
350             bundle.putString(RESTRICTION_KEY_ITEM_KEY, key);
351             bundle.putString(RESTRICTION_KEY_ITEM_VALUE, items.get(key));
352             bundles[i++] = bundle;
353         }
354         return bundles;
355     }
356 
removeItem(String key)357     private void removeItem(String key) {
358         Parcelable[] parcelables = mCurrentRestrictions.getParcelableArray(RESTRICTION_KEY_ITEMS);
359         if (parcelables != null) {
360             Map<String, String> items = new HashMap<>();
361             for (Parcelable parcelable : parcelables) {
362                 Bundle bundle = (Bundle) parcelable;
363                 if (!key.equals(bundle.getString(RESTRICTION_KEY_ITEM_KEY))) {
364                     items.put(bundle.getString(RESTRICTION_KEY_ITEM_KEY),
365                             bundle.getString(RESTRICTION_KEY_ITEM_VALUE));
366                 }
367             }
368             saveItems(getActivity(), items);
369         }
370     }
371 
372     /**
373      * Saves the value for the "cay_say_hello" restriction of AppRestrictionSchema.
374      *
375      * @param activity The activity
376      * @param allow    The value to be set for the restriction.
377      */
saveCanSayHello(Activity activity, boolean allow)378     private void saveCanSayHello(Activity activity, boolean allow) {
379         mCurrentRestrictions.putBoolean(RESTRICTION_KEY_SAY_HELLO, allow);
380         saveRestrictions(activity);
381         // Note that the owner app needs to remember the restrictions on its own.
382         editPreferences(activity).putBoolean(RESTRICTION_KEY_SAY_HELLO, allow).apply();
383     }
384 
385     /**
386      * Saves the value for the "message" restriction of AppRestrictionSchema.
387      *
388      * @param activity The activity
389      * @param message  The value to be set for the restriction.
390      */
saveMessage(Activity activity, String message)391     private void saveMessage(Activity activity, String message) {
392         mCurrentRestrictions.putString(RESTRICTION_KEY_MESSAGE, message);
393         saveRestrictions(activity);
394         editPreferences(activity).putString(RESTRICTION_KEY_MESSAGE, message).apply();
395     }
396 
397     /**
398      * Saves the value for the "number" restriction of AppRestrictionSchema.
399      *
400      * @param activity The activity
401      * @param number   The value to be set for the restriction.
402      */
saveNumber(Activity activity, int number)403     private void saveNumber(Activity activity, int number) {
404         mCurrentRestrictions.putInt(RESTRICTION_KEY_NUMBER, number);
405         saveRestrictions(activity);
406         editPreferences(activity).putInt(RESTRICTION_KEY_NUMBER, number).apply();
407     }
408 
409     /**
410      * Saves the value for the "rank" restriction of AppRestrictionSchema.
411      *
412      * @param activity The activity
413      * @param rank     The value to be set for the restriction.
414      */
saveRank(Activity activity, String rank)415     private void saveRank(Activity activity, String rank) {
416         mCurrentRestrictions.putString(RESTRICTION_KEY_RANK, rank);
417         saveRestrictions(activity);
418         editPreferences(activity).putString(RESTRICTION_KEY_RANK, rank).apply();
419     }
420 
addApproval(Activity activity, String approval)421     private void addApproval(Activity activity, String approval) {
422         List<String> approvals = new ArrayList<>(Arrays.asList(
423                 mCurrentRestrictions.getStringArray(RESTRICTION_KEY_APPROVALS)));
424         if (approvals.contains(approval)) {
425             return;
426         }
427         approvals.add(approval);
428         saveApprovals(activity, approvals.toArray(new String[approvals.size()]));
429     }
430 
removeApproval(Activity activity, String approval)431     private void removeApproval(Activity activity, String approval) {
432         List<String> approvals = new ArrayList<>(Arrays.asList(
433                 mCurrentRestrictions.getStringArray(RESTRICTION_KEY_APPROVALS)));
434         if (!approval.contains(approval)) {
435             return;
436         }
437         approvals.remove(approval);
438         saveApprovals(activity, approvals.toArray(new String[approvals.size()]));
439     }
440 
441     /**
442      * Saves the value for the "approvals" restriction of AppRestrictionSchema.
443      *
444      * @param activity  The activity
445      * @param approvals The value to be set for the restriction.
446      */
saveApprovals(Activity activity, String[] approvals)447     private void saveApprovals(Activity activity, String[] approvals) {
448         mCurrentRestrictions.putStringArray(RESTRICTION_KEY_APPROVALS, approvals);
449         saveRestrictions(activity);
450         editPreferences(activity).putString(RESTRICTION_KEY_APPROVALS,
451                 TextUtils.join(DELIMETER, approvals)).apply();
452     }
453 
454     /**
455      * Saves the value for the "items" restriction of AppRestrictionSchema.
456      *
457      * @param activity The activity.
458      * @param items    The values.
459      */
saveItems(Activity activity, Map<String, String> items)460     private void saveItems(Activity activity, Map<String, String> items) {
461         if (!BUNDLE_SUPPORTED) {
462             return;
463         }
464         mCurrentRestrictions.putParcelableArray(RESTRICTION_KEY_ITEMS, convertToBundles(items));
465         saveRestrictions(activity);
466         StringBuilder builder = new StringBuilder();
467         boolean first = true;
468         for (String key : items.keySet()) {
469             if (first) {
470                 first = false;
471             } else {
472                 builder.append(DELIMETER);
473             }
474             builder.append(key);
475             builder.append(SEPARATOR);
476             builder.append(items.get(key));
477         }
478         editPreferences(activity).putString(RESTRICTION_KEY_ITEMS, builder.toString()).apply();
479     }
480 
481     /**
482      * Saves all the restrictions.
483      *
484      * @param activity The activity.
485      */
saveRestrictions(Activity activity)486     private void saveRestrictions(Activity activity) {
487         DevicePolicyManager devicePolicyManager
488                 = (DevicePolicyManager) activity.getSystemService(Context.DEVICE_POLICY_SERVICE);
489         devicePolicyManager.setApplicationRestrictions(
490                 EnforcerDeviceAdminReceiver.getComponentName(activity),
491                 Constants.PACKAGE_NAME_APP_RESTRICTION_SCHEMA, mCurrentRestrictions);
492     }
493 
editPreferences(Activity activity)494     private SharedPreferences.Editor editPreferences(Activity activity) {
495         return activity.getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE).edit();
496     }
497 
498     /**
499      * Sequential search
500      *
501      * @param array The string array
502      * @param s     The string to search for
503      * @return Index if found. -1 if not found.
504      */
search(String[] array, String s)505     private int search(String[] array, String s) {
506         for (int i = 0; i < array.length; ++i) {
507             if (s.equals(array[i])) {
508                 return i;
509             }
510         }
511         return -1;
512     }
513 
514 }
515