1 /*
2  * Copyright (C) 2015 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.android.settings.applications.intentpicker;
18 
19 import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE;
20 import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_SELECTED;
21 import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_VERIFIED;
22 
23 import android.app.Activity;
24 import android.app.settings.SettingsEnums;
25 import android.appwidget.AppWidgetManager;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.PackageManager;
29 import android.content.pm.verify.domain.DomainVerificationManager;
30 import android.content.pm.verify.domain.DomainVerificationUserState;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.util.ArraySet;
34 import android.util.Log;
35 import android.view.LayoutInflater;
36 import android.view.View;
37 import android.widget.Switch;
38 import android.widget.TextView;
39 
40 import androidx.annotation.VisibleForTesting;
41 import androidx.appcompat.app.AlertDialog;
42 import androidx.preference.Preference;
43 import androidx.preference.PreferenceCategory;
44 
45 import com.android.settings.R;
46 import com.android.settings.Utils;
47 import com.android.settings.applications.AppInfoBase;
48 import com.android.settings.applications.ClearDefaultsPreference;
49 import com.android.settings.widget.EntityHeaderController;
50 import com.android.settingslib.applications.AppUtils;
51 import com.android.settingslib.widget.FooterPreference;
52 import com.android.settingslib.widget.MainSwitchPreference;
53 import com.android.settingslib.widget.OnMainSwitchChangeListener;
54 
55 import java.util.List;
56 import java.util.Set;
57 import java.util.UUID;
58 
59 /** The page of the Open by default */
60 public class AppLaunchSettings extends AppInfoBase implements
61         Preference.OnPreferenceChangeListener, OnMainSwitchChangeListener {
62     private static final String TAG = "AppLaunchSettings";
63     // Preference keys
64     private static final String MAIN_SWITCH_PREF_KEY = "open_by_default_supported_links";
65     private static final String VERIFIED_LINKS_PREF_KEY = "open_by_default_verified_links";
66     private static final String ADD_LINK_PREF_KEY = "open_by_default_add_link";
67     private static final String CLEAR_DEFAULTS_PREF_KEY = "app_launch_clear_defaults";
68     private static final String FOOTER_PREF_KEY = "open_by_default_footer";
69 
70     private static final String MAIN_PREF_CATEGORY_KEY = "open_by_default_main_category";
71     private static final String SELECTED_LINKS_CATEGORY_KEY =
72             "open_by_default_selected_links_category";
73     private static final String OTHER_DETAILS_PREF_CATEGORY_KEY = "app_launch_other_defaults";
74 
75     private static final String LEARN_MORE_URI =
76             "https://developer.android.com/training/app-links/verify-site-associations";
77 
78     // Dialogs id
79     private static final int DLG_VERIFIED_LINKS = DLG_BASE + 1;
80 
81     // Arguments key
82     public static final String APP_PACKAGE_KEY = "app_package";
83 
84     private ClearDefaultsPreference mClearDefaultsPreference;
85     private MainSwitchPreference mMainSwitchPreference;
86     private Preference mAddLinkPreference;
87     private PreferenceCategory mMainPreferenceCategory;
88     private PreferenceCategory mSelectedLinksPreferenceCategory;
89     private PreferenceCategory mOtherDefaultsPreferenceCategory;
90 
91     private boolean mActivityCreated;
92 
93     @VisibleForTesting
94     Context mContext;
95     @VisibleForTesting
96     DomainVerificationManager mDomainVerificationManager;
97 
98     @Override
onAttach(Context context)99     public void onAttach(Context context) {
100         super.onAttach(context);
101         mContext = context;
102         mActivityCreated = false;
103     }
104 
105     @Override
onCreate(Bundle savedInstanceState)106     public void onCreate(Bundle savedInstanceState) {
107         super.onCreate(savedInstanceState);
108         if (mAppEntry == null) {
109             Log.w(TAG, "onCreate: mAppEntry is null, please check the reason!!!");
110             getActivity().finish();
111             return;
112         }
113         addPreferencesFromResource(R.xml.installed_app_launch_settings);
114         mDomainVerificationManager = mContext.getSystemService(DomainVerificationManager.class);
115         initUIComponents();
116     }
117 
118     @Override
onViewCreated(View view, Bundle savedInstanceState)119     public void onViewCreated(View view, Bundle savedInstanceState) {
120         super.onViewCreated(view, savedInstanceState);
121         createHeaderPreference();
122     }
123 
124     @Override
getMetricsCategory()125     public int getMetricsCategory() {
126         return SettingsEnums.APPLICATIONS_APP_LAUNCH;
127     }
128 
129     @Override
createDialog(int id, int errorCode)130     protected AlertDialog createDialog(int id, int errorCode) {
131         if (id == DLG_VERIFIED_LINKS) {
132             return createVerifiedLinksDialog();
133         }
134         return null;
135     }
136 
137     @Override
refreshUi()138     protected boolean refreshUi() {
139         mClearDefaultsPreference.setPackageName(mPackageName);
140         mClearDefaultsPreference.setAppEntry(mAppEntry);
141         return true;
142     }
143 
144     @Override
onPreferenceChange(Preference preference, Object newValue)145     public boolean onPreferenceChange(Preference preference, Object newValue) {
146         final boolean isChecked = (boolean) newValue;
147         IntentPickerUtils.logd(
148                 "onPreferenceChange: " + preference.getTitle() + " isChecked: " + isChecked);
149         if ((preference instanceof LeftSideCheckBoxPreference) && !isChecked) {
150             final Set<String> domainSet = new ArraySet<>();
151             domainSet.add(preference.getTitle().toString());
152             removePreference(preference.getKey());
153             final DomainVerificationUserState userState =
154                     IntentPickerUtils.getDomainVerificationUserState(mDomainVerificationManager,
155                             mPackageName);
156             if (userState == null) {
157                 return false;
158             }
159             setDomainVerificationUserSelection(userState.getIdentifier(), domainSet, /* enabled= */
160                     false);
161             mAddLinkPreference.setEnabled(isAddLinksNotEmpty());
162         }
163         return true;
164     }
165 
166     @Override
onSwitchChanged(Switch switchView, boolean isChecked)167     public void onSwitchChanged(Switch switchView, boolean isChecked) {
168         IntentPickerUtils.logd("onSwitchChanged: isChecked=" + isChecked);
169         if (mMainSwitchPreference != null) { //mMainSwitchPreference synced with Switch
170             mMainSwitchPreference.setChecked(isChecked);
171         }
172         if (mMainPreferenceCategory != null) {
173             mMainPreferenceCategory.setVisible(isChecked);
174         }
175         if (mDomainVerificationManager != null) {
176             try {
177                 mDomainVerificationManager.setDomainVerificationLinkHandlingAllowed(mPackageName,
178                         isChecked);
179             } catch (PackageManager.NameNotFoundException e) {
180                 Log.w(TAG, "onSwitchChanged: " + e.getMessage());
181             }
182         }
183     }
184 
createHeaderPreference()185     private void createHeaderPreference() {
186         if (mActivityCreated) {
187             Log.w(TAG, "onParentActivityCreated: ignoring duplicate call.");
188             return;
189         }
190         mActivityCreated = true;
191         if (mPackageInfo == null) {
192             Log.w(TAG, "onParentActivityCreated: PakcageInfo is null.");
193             return;
194         }
195         final Activity activity = getActivity();
196         final String summary = activity.getString(R.string.app_launch_top_intro_message);
197         final Preference pref = EntityHeaderController
198                 .newInstance(activity, this, null /* header */)
199                 .setRecyclerView(getListView(), getSettingsLifecycle())
200                 .setIcon(Utils.getBadgedIcon(mContext, mPackageInfo.applicationInfo))
201                 .setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
202                 .setSummary(summary)  // add intro text
203                 .setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
204                 .setPackageName(mPackageName)
205                 .setUid(mPackageInfo.applicationInfo.uid)
206                 .setHasAppInfoLink(true)
207                 .setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
208                         EntityHeaderController.ActionType.ACTION_NONE)
209                 .done(activity, getPrefContext());
210         getPreferenceScreen().addPreference(pref);
211     }
212 
initUIComponents()213     private void initUIComponents() {
214         initMainSwitchAndCategories();
215         if (canUpdateMainSwitchAndCategories()) {
216             initVerifiedLinksPreference();
217             initAddLinkPreference();
218             addSelectedLinksPreference();
219             initFooter();
220         }
221     }
222 
initMainSwitchAndCategories()223     private void initMainSwitchAndCategories() {
224         mMainSwitchPreference = (MainSwitchPreference) findPreference(MAIN_SWITCH_PREF_KEY);
225         mMainPreferenceCategory = findPreference(MAIN_PREF_CATEGORY_KEY);
226         mSelectedLinksPreferenceCategory = findPreference(SELECTED_LINKS_CATEGORY_KEY);
227         // Initialize the "Other Default Category" section
228         initOtherDefaultsSection();
229     }
230 
canUpdateMainSwitchAndCategories()231     private boolean canUpdateMainSwitchAndCategories() {
232         final DomainVerificationUserState userState =
233                 IntentPickerUtils.getDomainVerificationUserState(mDomainVerificationManager,
234                         mPackageName);
235         if (userState == null) {
236             disabledPreference();
237             return false;
238         }
239 
240         IntentPickerUtils.logd("isLinkHandlingAllowed() : " + userState.isLinkHandlingAllowed());
241         mMainSwitchPreference.updateStatus(userState.isLinkHandlingAllowed());
242         mMainSwitchPreference.addOnSwitchChangeListener(this);
243         mMainPreferenceCategory.setVisible(userState.isLinkHandlingAllowed());
244         return true;
245     }
246 
247     /** Initialize verified links preference */
initVerifiedLinksPreference()248     private void initVerifiedLinksPreference() {
249         final VerifiedLinksPreference verifiedLinksPreference =
250                 (VerifiedLinksPreference) mMainPreferenceCategory.findPreference(
251                         VERIFIED_LINKS_PREF_KEY);
252         verifiedLinksPreference.setWidgetFrameClickListener(l -> {
253             showVerifiedLinksDialog();
254         });
255         final int verifiedLinksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
256         verifiedLinksPreference.setTitle(getVerifiedLinksTitle(verifiedLinksNo));
257         verifiedLinksPreference.setCheckBoxVisible(verifiedLinksNo > 0);
258         verifiedLinksPreference.setEnabled(verifiedLinksNo > 0);
259     }
260 
showVerifiedLinksDialog()261     private void showVerifiedLinksDialog() {
262         final int linksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
263         if (linksNo == 0) {
264             return;
265         }
266         showDialogInner(DLG_VERIFIED_LINKS, /* moveErrorCode= */ 0);
267     }
268 
createVerifiedLinksDialog()269     private AlertDialog createVerifiedLinksDialog() {
270         final int linksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
271 
272         final View titleView = LayoutInflater.from(mContext).inflate(
273                 R.layout.app_launch_verified_links_title, /* root= */ null);
274         ((TextView) titleView.findViewById(R.id.dialog_title)).setText(
275                 getVerifiedLinksTitle(linksNo));
276         ((TextView) titleView.findViewById(R.id.dialog_message)).setText(
277                 getVerifiedLinksMessage(linksNo));
278 
279         final List<String> verifiedLinksList = IntentPickerUtils.getLinksList(
280                 mDomainVerificationManager, mPackageName, DOMAIN_STATE_VERIFIED);
281         return new AlertDialog.Builder(mContext)
282                 .setCustomTitle(titleView)
283                 .setCancelable(true)
284                 .setItems(verifiedLinksList.toArray(new String[0]), /* listener= */ null)
285                 .setPositiveButton(R.string.app_launch_dialog_ok, /* listener= */ null)
286                 .create();
287     }
288 
289     @VisibleForTesting
getVerifiedLinksTitle(int linksNo)290     String getVerifiedLinksTitle(int linksNo) {
291         return getResources().getQuantityString(
292                 R.plurals.app_launch_verified_links_title, linksNo, linksNo);
293     }
294 
getVerifiedLinksMessage(int linksNo)295     private String getVerifiedLinksMessage(int linksNo) {
296         return getResources().getQuantityString(
297                 R.plurals.app_launch_verified_links_message, linksNo, linksNo);
298     }
299 
300     /** Add selected links items */
addSelectedLinksPreference()301     void addSelectedLinksPreference() {
302         if (getLinksNumber(DOMAIN_STATE_SELECTED) == 0) {
303             return;
304         }
305         mSelectedLinksPreferenceCategory.removeAll();
306         final List<String> selectedLinks = IntentPickerUtils.getLinksList(
307                 mDomainVerificationManager, mPackageName, DOMAIN_STATE_SELECTED);
308         for (String host : selectedLinks) {
309             generateCheckBoxPreference(mSelectedLinksPreferenceCategory, host);
310         }
311         mAddLinkPreference.setEnabled(isAddLinksNotEmpty());
312     }
313 
314     /** Initialize add link preference */
initAddLinkPreference()315     private void initAddLinkPreference() {
316         mAddLinkPreference = findPreference(ADD_LINK_PREF_KEY);
317         mAddLinkPreference.setVisible(isAddLinksShown());
318         mAddLinkPreference.setEnabled(isAddLinksNotEmpty());
319         mAddLinkPreference.setOnPreferenceClickListener(preference -> {
320             final int stateNoneLinksNo = getLinksNumber(DOMAIN_STATE_NONE);
321             IntentPickerUtils.logd("The number of the state none links: " + stateNoneLinksNo);
322             if (stateNoneLinksNo > 0) {
323                 showProgressDialogFragment();
324             }
325             return true;
326         });
327     }
328 
isAddLinksNotEmpty()329     private boolean isAddLinksNotEmpty() {
330         return getLinksNumber(DOMAIN_STATE_NONE) > 0;
331     }
332 
isAddLinksShown()333     private boolean isAddLinksShown() {
334         return (isAddLinksNotEmpty() || getLinksNumber(DOMAIN_STATE_SELECTED) > 0);
335     }
336 
showProgressDialogFragment()337     private void showProgressDialogFragment() {
338         final Bundle args = new Bundle();
339         args.putString(APP_PACKAGE_KEY, mPackageName);
340         final ProgressDialogFragment dialogFragment = new ProgressDialogFragment();
341         dialogFragment.setArguments(args);
342         dialogFragment.showDialog(getActivity().getSupportFragmentManager());
343     }
344 
disabledPreference()345     private void disabledPreference() {
346         mMainSwitchPreference.updateStatus(false);
347         mMainSwitchPreference.setSelectable(false);
348         mMainSwitchPreference.setEnabled(false);
349         mMainPreferenceCategory.setVisible(false);
350     }
351 
352     /** Init OTHER DEFAULTS category */
initOtherDefaultsSection()353     private void initOtherDefaultsSection() {
354         mOtherDefaultsPreferenceCategory = findPreference(OTHER_DETAILS_PREF_CATEGORY_KEY);
355         mOtherDefaultsPreferenceCategory.setVisible(isClearDefaultsEnabled());
356         mClearDefaultsPreference = (ClearDefaultsPreference) findPreference(
357                 CLEAR_DEFAULTS_PREF_KEY);
358     }
359 
initFooter()360     private void initFooter() {
361         final CharSequence footerText = mContext.getText(R.string.app_launch_footer);
362         final FooterPreference footerPreference = (FooterPreference) findPreference(
363                 FOOTER_PREF_KEY);
364         footerPreference.setTitle(footerText);
365         // learn more
366         footerPreference.setLearnMoreAction(view -> {
367             final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(LEARN_MORE_URI));
368             mContext.startActivity(intent);
369         });
370         final String learnMoreContentDescription = mContext.getString(
371                 R.string.footer_learn_more_content_description, getLabelName());
372         footerPreference.setLearnMoreContentDescription(learnMoreContentDescription);
373     }
374 
getLabelName()375     private String getLabelName() {
376         return mContext.getString(R.string.launch_by_default);
377     }
378 
isClearDefaultsEnabled()379     private boolean isClearDefaultsEnabled() {
380         final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
381         final boolean hasBindAppWidgetPermission =
382                 appWidgetManager.hasBindAppWidgetPermission(mAppEntry.info.packageName);
383 
384         final boolean isAutoLaunchEnabled = AppUtils.hasPreferredActivities(mPm, mPackageName)
385                 || AppUtils.isDefaultBrowser(mContext, mPackageName)
386                 || AppUtils.hasUsbDefaults(mUsbManager, mPackageName);
387 
388         IntentPickerUtils.logd("isClearDefaultsEnabled hasBindAppWidgetPermission : "
389                 + hasBindAppWidgetPermission);
390         IntentPickerUtils.logd(
391                 "isClearDefaultsEnabled isAutoLaunchEnabled : " + isAutoLaunchEnabled);
392         return (isAutoLaunchEnabled || hasBindAppWidgetPermission);
393     }
394 
setDomainVerificationUserSelection(UUID identifier, Set<String> domainSet, boolean isEnabled)395     private void setDomainVerificationUserSelection(UUID identifier, Set<String> domainSet,
396             boolean isEnabled) {
397         try {
398             mDomainVerificationManager.setDomainVerificationUserSelection(identifier, domainSet,
399                     isEnabled);
400         } catch (PackageManager.NameNotFoundException e) {
401             Log.w(TAG, "addSelectedItems : " + e.getMessage());
402         }
403     }
404 
generateCheckBoxPreference(PreferenceCategory parent, String title)405     private void generateCheckBoxPreference(PreferenceCategory parent, String title) {
406         final LeftSideCheckBoxPreference checkBoxPreference = new LeftSideCheckBoxPreference(
407                 parent.getContext(), /* isChecked= */ true);
408         checkBoxPreference.setTitle(title);
409         checkBoxPreference.setOnPreferenceChangeListener(this);
410         checkBoxPreference.setKey(UUID.randomUUID().toString());
411         parent.addPreference(checkBoxPreference);
412     }
413 
414     /** get the number of the specify links */
getLinksNumber(@omainVerificationUserState.DomainState int state)415     private int getLinksNumber(@DomainVerificationUserState.DomainState int state) {
416         final List<String> linkList = IntentPickerUtils.getLinksList(
417                 mDomainVerificationManager, mPackageName, state);
418         if (linkList == null) {
419             return 0;
420         }
421         return linkList.size();
422     }
423 }
424