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