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