Implement new "Open by default" page

- Use the new MainSwitch and TopIntro widgets.
- Create VerifiedLinksPreference and LeftSideCheckBoxPreference.
- Create ProgressDialogFragment and SupportedLinksDialogFragment.
- Retrieve the links info from DomainVerificationManager.
- Allow user to change the supported links into selected links.
- Refactor the ClearDefaultsPreference.
- Move AppLaunchSettings into intentpicker directory.

Bug: 177923646
Bug: 182530528
Test: manual test
Change-Id: I935a2fdd0f62cdb8b8d3210fb2f800f682249cb7
This commit is contained in:
Sunny Shao
2021-03-05 19:51:08 +08:00
parent 01f77d8e00
commit 81fa20a4f1
21 changed files with 1478 additions and 183 deletions

View File

@@ -0,0 +1,404 @@
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE;
import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_SELECTED;
import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_VERIFIED;
import android.app.Activity;
import android.app.settings.SettingsEnums;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.net.Uri;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AlertDialog;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.applications.AppInfoBase;
import com.android.settings.applications.ClearDefaultsPreference;
import com.android.settings.utils.AnnotationSpan;
import com.android.settings.widget.EntityHeaderController;
import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.widget.FooterPreference;
import com.android.settingslib.widget.MainSwitchPreference;
import com.android.settingslib.widget.OnMainSwitchChangeListener;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/** The page of the Open by default */
public class AppLaunchSettings extends AppInfoBase implements
Preference.OnPreferenceChangeListener, OnMainSwitchChangeListener {
private static final String TAG = "AppLaunchSettings";
// Preference keys
private static final String MAIN_SWITCH_PREF_KEY = "open_by_default_supported_links";
private static final String VERIFIED_LINKS_PREF_KEY = "open_by_default_verified_links";
private static final String ADD_LINK_PREF_KEY = "open_by_default_add_link";
private static final String CLEAR_DEFAULTS_PREF_KEY = "app_launch_clear_defaults";
private static final String FOOTER_PREF_KEY = "open_by_default_footer";
private static final String MAIN_PREF_CATEGORY_KEY = "open_by_default_main_category";
private static final String SELECTED_LINKS_CATEGORY_KEY =
"open_by_default_selected_links_category";
private static final String OTHER_DETAILS_PREF_CATEGORY_KEY = "app_launch_other_defaults";
// Url and Uri
private static final String ANNOTATION_URL = "url";
private static final String LEARN_MORE_URI =
"https://developer.android.com/training/app-links/verify-site-associations";
// Dialogs id
private static final int DLG_VERIFIED_LINKS = DLG_BASE + 1;
// Arguments key
public static final String APP_PACKAGE_KEY = "app_package";
private ClearDefaultsPreference mClearDefaultsPreference;
private MainSwitchPreference mMainSwitchPreference;
private PreferenceCategory mMainPreferenceCategory;
private PreferenceCategory mSelectedLinksPreferenceCategory;
private PreferenceCategory mOtherDefaultsPreferenceCategory;
private boolean mActivityCreated;
@VisibleForTesting
Context mContext;
@VisibleForTesting
DomainVerificationManager mDomainVerificationManager;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mContext = context;
mActivityCreated = false;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.installed_app_launch_settings);
mDomainVerificationManager = mContext.getSystemService(DomainVerificationManager.class);
initUIComponents();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
createHeaderPreference();
}
@Override
public int getMetricsCategory() {
return SettingsEnums.APPLICATIONS_APP_LAUNCH;
}
@Override
protected AlertDialog createDialog(int id, int errorCode) {
if (id == DLG_VERIFIED_LINKS) {
return createVerifiedLinksDialog();
}
return null;
}
@Override
protected boolean refreshUi() {
mClearDefaultsPreference.setPackageName(mPackageName);
mClearDefaultsPreference.setAppEntry(mAppEntry);
return true;
}
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
final boolean isChecked = (boolean) newValue;
IntentPickerUtils.logd(
"onPreferenceChange: " + preference.getTitle() + " isChecked: " + isChecked);
if ((preference instanceof LeftSideCheckBoxPreference) && !isChecked) {
final Set<String> domainSet = new ArraySet<>();
domainSet.add(preference.getTitle().toString());
removePreference(preference.getKey());
final DomainVerificationUserState userState =
IntentPickerUtils.getDomainVerificationUserState(mDomainVerificationManager,
mPackageName);
if (userState == null) {
return false;
}
setDomainVerificationUserSelection(userState.getIdentifier(), domainSet, /* enabled= */
false);
}
return true;
}
@Override
public void onSwitchChanged(Switch switchView, boolean isChecked) {
IntentPickerUtils.logd("onSwitchChanged: isChecked=" + isChecked);
if (mMainSwitchPreference != null) { //mMainSwitchPreference synced with Switch
mMainSwitchPreference.setChecked(isChecked);
}
if (mMainPreferenceCategory != null) {
mMainPreferenceCategory.setVisible(isChecked);
}
if (mDomainVerificationManager != null) {
try {
mDomainVerificationManager.setDomainVerificationLinkHandlingAllowed(mPackageName,
isChecked);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "onSwitchChanged: " + e.getMessage());
}
}
}
private void createHeaderPreference() {
if (mActivityCreated) {
Log.w(TAG, "onParentActivityCreated: ignoring duplicate call.");
return;
}
mActivityCreated = true;
if (mPackageInfo == null) {
Log.w(TAG, "onParentActivityCreated: PakcageInfo is null.");
return;
}
final Activity activity = getActivity();
final Preference pref = EntityHeaderController
.newInstance(activity, this, null /* header */)
.setRecyclerView(getListView(), getSettingsLifecycle())
.setIcon(Utils.getBadgedIcon(mContext, mPackageInfo.applicationInfo))
.setLabel(mPackageInfo.applicationInfo.loadLabel(mPm))
.setSummary("" /* summary */) // no version number
.setIsInstantApp(AppUtils.isInstant(mPackageInfo.applicationInfo))
.setPackageName(mPackageName)
.setUid(mPackageInfo.applicationInfo.uid)
.setHasAppInfoLink(true)
.setButtonActions(EntityHeaderController.ActionType.ACTION_NONE,
EntityHeaderController.ActionType.ACTION_NONE)
.done(activity, getPrefContext());
getPreferenceScreen().addPreference(pref);
}
private void initUIComponents() {
initMainSwitchAndCategories();
if (canUpdateMainSwitchAndCategories()) {
initVerifiedLinksPreference();
addSelectedLinksPreference();
initAddLinkPreference();
initFooter();
}
}
private void initMainSwitchAndCategories() {
mMainSwitchPreference = (MainSwitchPreference) findPreference(MAIN_SWITCH_PREF_KEY);
mMainPreferenceCategory = findPreference(MAIN_PREF_CATEGORY_KEY);
mSelectedLinksPreferenceCategory = findPreference(SELECTED_LINKS_CATEGORY_KEY);
// Initialize the "Other Default Category" section
initOtherDefaultsSection();
}
private boolean canUpdateMainSwitchAndCategories() {
final DomainVerificationUserState userState =
IntentPickerUtils.getDomainVerificationUserState(mDomainVerificationManager,
mPackageName);
if (userState == null) {
disabledPreference();
return false;
}
IntentPickerUtils.logd("isLinkHandlingAllowed() : " + userState.isLinkHandlingAllowed());
mMainSwitchPreference.updateStatus(userState.isLinkHandlingAllowed());
mMainSwitchPreference.addOnSwitchChangeListener(this);
mMainPreferenceCategory.setVisible(userState.isLinkHandlingAllowed());
return true;
}
/** Initialize verified links preference */
private void initVerifiedLinksPreference() {
final VerifiedLinksPreference verifiedLinksPreference =
(VerifiedLinksPreference) mMainPreferenceCategory.findPreference(
VERIFIED_LINKS_PREF_KEY);
verifiedLinksPreference.setWidgetFrameClickListener(l -> {
showVerifiedLinksDialog();
});
final int verifiedLinksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
verifiedLinksPreference.setTitle(getVerifiedLinksTitle(verifiedLinksNo));
verifiedLinksPreference.setCheckBoxVisible(verifiedLinksNo > 0);
verifiedLinksPreference.setEnabled(verifiedLinksNo > 0);
}
private void showVerifiedLinksDialog() {
final int linksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
if (linksNo == 0) {
return;
}
showDialogInner(DLG_VERIFIED_LINKS, /* moveErrorCode= */ 0);
}
private AlertDialog createVerifiedLinksDialog() {
final int linksNo = getLinksNumber(DOMAIN_STATE_VERIFIED);
final View titleView = LayoutInflater.from(mContext).inflate(
R.layout.app_launch_verified_links_title, /* root= */ null);
((TextView) titleView.findViewById(R.id.dialog_title)).setText(
getVerifiedLinksTitle(linksNo));
((TextView) titleView.findViewById(R.id.dialog_message)).setText(
getVerifiedLinksMessage(linksNo));
final List<String> verifiedLinksList = IntentPickerUtils.getLinksList(
mDomainVerificationManager, mPackageName, DOMAIN_STATE_VERIFIED);
return new AlertDialog.Builder(mContext)
.setCustomTitle(titleView)
.setCancelable(true)
.setItems(verifiedLinksList.toArray(new String[0]), /* listener= */ null)
.setPositiveButton(R.string.app_launch_dialog_ok, /* listener= */ null)
.create();
}
@VisibleForTesting
String getVerifiedLinksTitle(int linksNo) {
return getResources().getQuantityString(
R.plurals.app_launch_verified_links_title, linksNo, linksNo);
}
private String getVerifiedLinksMessage(int linksNo) {
return getResources().getQuantityString(
R.plurals.app_launch_verified_links_message, linksNo, linksNo);
}
/** Add selected links items */
public void addSelectedLinksPreference() {
if (getLinksNumber(DOMAIN_STATE_SELECTED) == 0) {
return;
}
mSelectedLinksPreferenceCategory.removeAll();
final List<String> selectedLinks = IntentPickerUtils.getLinksList(
mDomainVerificationManager, mPackageName, DOMAIN_STATE_SELECTED);
for (String host : selectedLinks) {
generateCheckBoxPreference(mSelectedLinksPreferenceCategory, host);
}
}
/** Initialize add link preference */
private void initAddLinkPreference() {
final Preference addLinkPreference = findPreference(ADD_LINK_PREF_KEY);
if (addLinkPreference != null) {
addLinkPreference.setEnabled(getLinksNumber(DOMAIN_STATE_NONE) > 0);
addLinkPreference.setOnPreferenceClickListener(preference -> {
final int stateNoneLinksNo = getLinksNumber(DOMAIN_STATE_NONE);
IntentPickerUtils.logd("The number of the state none links: " + stateNoneLinksNo);
if (stateNoneLinksNo > 0) {
showProgressDialogFragment();
}
return true;
});
}
}
private void showProgressDialogFragment() {
final Bundle args = new Bundle();
args.putString(APP_PACKAGE_KEY, mPackageName);
final ProgressDialogFragment dialogFragment = new ProgressDialogFragment();
dialogFragment.setArguments(args);
dialogFragment.showDialog(getActivity().getSupportFragmentManager());
}
private void disabledPreference() {
mMainSwitchPreference.updateStatus(false);
mMainSwitchPreference.setSelectable(false);
mMainSwitchPreference.setEnabled(false);
mMainPreferenceCategory.setVisible(false);
}
/** Init OTHER DEFAULTS category */
private void initOtherDefaultsSection() {
mOtherDefaultsPreferenceCategory = findPreference(OTHER_DETAILS_PREF_CATEGORY_KEY);
mOtherDefaultsPreferenceCategory.setVisible(isClearDefaultsEnabled());
mClearDefaultsPreference = (ClearDefaultsPreference) findPreference(
CLEAR_DEFAULTS_PREF_KEY);
}
private void initFooter() {
// learn more
final AnnotationSpan.LinkInfo linkInfo =
new AnnotationSpan.LinkInfo(ANNOTATION_URL, v -> {
final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(LEARN_MORE_URI));
mContext.startActivity(intent);
});
final CharSequence footerText = mContext.getText(R.string.app_launch_footer);
final FooterPreference footerPreference = (FooterPreference) findPreference(
FOOTER_PREF_KEY);
footerPreference.setTitle(AnnotationSpan.linkify(footerText, linkInfo));
}
private boolean isClearDefaultsEnabled() {
final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(mContext);
final boolean hasBindAppWidgetPermission =
appWidgetManager.hasBindAppWidgetPermission(mAppEntry.info.packageName);
final boolean isAutoLaunchEnabled = AppUtils.hasPreferredActivities(mPm, mPackageName)
|| AppUtils.isDefaultBrowser(mContext, mPackageName)
|| AppUtils.hasUsbDefaults(mUsbManager, mPackageName);
IntentPickerUtils.logd("isClearDefaultsEnabled hasBindAppWidgetPermission : "
+ hasBindAppWidgetPermission);
IntentPickerUtils.logd(
"isClearDefaultsEnabled isAutoLaunchEnabled : " + isAutoLaunchEnabled);
return (isAutoLaunchEnabled || hasBindAppWidgetPermission);
}
private void setDomainVerificationUserSelection(UUID identifier, Set<String> domainSet,
boolean isEnabled) {
try {
mDomainVerificationManager.setDomainVerificationUserSelection(identifier, domainSet,
isEnabled);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "addSelectedItems : " + e.getMessage());
}
}
private void generateCheckBoxPreference(PreferenceCategory parent, String title) {
final LeftSideCheckBoxPreference checkBoxPreference = new LeftSideCheckBoxPreference(
parent.getContext(), /* isChecked= */ true);
checkBoxPreference.setTitle(title);
checkBoxPreference.setOnPreferenceChangeListener(this);
checkBoxPreference.setKey(UUID.randomUUID().toString());
parent.addPreference(checkBoxPreference);
}
/** get the number of the specify links */
private int getLinksNumber(@DomainVerificationUserState.DomainState int state) {
final List<String> linkList = IntentPickerUtils.getLinksList(
mDomainVerificationManager, mPackageName, state);
if (linkList == null) {
return 0;
}
return linkList.size();
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.os.Build;
import android.text.Layout;
import android.text.SpannableString;
import android.text.style.AlignmentSpan;
import android.util.Log;
import java.util.List;
import java.util.stream.Collectors;
/** The common APIs for intent picker */
public class IntentPickerUtils {
private static final String TAG = "IntentPickerUtils";
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
private IntentPickerUtils() {
}
/**
* Gets the centralized title.
*
* @param title The title of the dialog box.
* @return The spannable string with centralized title.
*/
public static SpannableString getCentralizedDialogTitle(String title) {
final SpannableString dialogTitle = new SpannableString(title);
dialogTitle.setSpan(new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER), /* start= */
0, title.length(), /* flags= */ 0);
return dialogTitle;
}
/**
* Gets the {@link DomainVerificationUserState} for specific application.
*
* @param manager The {@link DomainVerificationManager}.
* @param pkgName The package name of the target application.
*/
public static DomainVerificationUserState getDomainVerificationUserState(
DomainVerificationManager manager, String pkgName) {
try {
final DomainVerificationUserState domainVerificationUserState =
manager.getDomainVerificationUserState(pkgName);
return domainVerificationUserState;
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, e.getMessage());
return null;
}
}
/**
* Gets the links list by {@link DomainVerificationUserState.DomainState}
*
* @param manager The {@link DomainVerificationManager}.
* @param pkgName The package name of the target application.
* @param state The user state you want to query.
* @return A links list.
*/
public static List<String> getLinksList(DomainVerificationManager manager, String pkgName,
@DomainVerificationUserState.DomainState int state) {
final DomainVerificationUserState userStage = getDomainVerificationUserState(manager,
pkgName);
if (userStage == null) {
return null;
}
return userStage.getHostToStateMap()
.entrySet()
.stream()
.filter(it -> it.getValue() == state)
.map(it -> it.getKey())
.collect(Collectors.toList());
}
/** Logs the message in debug ROM. */
public static void logd(String msg) {
if (DEBUG) {
Log.d(TAG, msg);
}
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.CheckBox;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
/** This preference has a check box in the left side. */
public class LeftSideCheckBoxPreference extends TwoTargetPreference {
private boolean mChecked;
private CheckBox mCheckBox;
public LeftSideCheckBoxPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setLayoutResource(R.layout.preference_checkable_two_target);
}
public LeftSideCheckBoxPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public LeftSideCheckBoxPreference(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public LeftSideCheckBoxPreference(Context context) {
this(context, /* attrs= */ null);
}
public LeftSideCheckBoxPreference(Context context, boolean isChecked) {
super(context);
mChecked = isChecked;
setLayoutResource(R.layout.preference_checkable_two_target);
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
mCheckBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox);
if (mCheckBox != null) {
mCheckBox.setChecked(mChecked);
}
}
@Override
protected void onClick() {
if (mCheckBox != null) {
mChecked = !mChecked;
mCheckBox.setChecked(mChecked);
callChangeListener(mChecked);
}
}
}

View File

@@ -0,0 +1,211 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import static android.content.pm.verify.domain.DomainVerificationUserState.DOMAIN_STATE_NONE;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.verify.domain.DomainOwner;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import com.android.settings.R;
import com.android.settingslib.utils.ThreadUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** A customized {@link DialogFragment} with a progress bar. */
public class ProgressDialogFragment extends DialogFragment {
private static final String TAG = "ProgressDialogFragment";
private static final String DLG_ID = "ProgressDialog";
private static final int PROGRESS_BAR_STEPPING_TIME = 20;
private ProgressAlertDialog mProgressAlertDialog;
private DomainVerificationManager mDomainVerificationManager;
private List<SupportedLinkWrapper> mSupportedLinkWrapperList;
private SupportedLinkViewModel mViewModel;
private Handler mHandle;
private String mPackage;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mViewModel = ViewModelProviders.of(this.getActivity()).get(SupportedLinkViewModel.class);
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
mPackage = getArguments().getString(AppLaunchSettings.APP_PACKAGE_KEY);
mDomainVerificationManager = getActivity().getSystemService(
DomainVerificationManager.class);
mHandle = new Handler(Looper.getMainLooper());
mProgressAlertDialog = createProgressAlertDialog();
return mProgressAlertDialog;
}
private ProgressAlertDialog createProgressAlertDialog() {
final Context context = getActivity();
final ProgressAlertDialog progressDialog = new ProgressAlertDialog(context);
final String title = context.getResources().getString(
R.string.app_launch_checking_links_title);
progressDialog.setTitle(IntentPickerUtils.getCentralizedDialogTitle(title));
progressDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
context.getText(R.string.app_launch_dialog_cancel),
(dialog, which) -> {
if (which == DialogInterface.BUTTON_NEGATIVE) {
dialog.cancel();
}
});
progressDialog.setCanceledOnTouchOutside(true);
return progressDialog;
}
/** Display the {@link ProgressAlertDialog}. */
public void showDialog(FragmentManager manager) {
show(manager, DLG_ID);
}
@Override
public void onResume() {
super.onResume();
generateProgressAlertDialog();
}
@Override
public void onDestroy() {
super.onDestroy();
if (mProgressAlertDialog != null && mProgressAlertDialog.isShowing()) {
mProgressAlertDialog.cancel();
}
}
/**
* To generate a progress alter dialog and invoke the supported links dialog.
*/
private void generateProgressAlertDialog() {
ThreadUtils.postOnBackgroundThread(() -> {
final long start = SystemClock.elapsedRealtime();
queryLinksInBackground();
IntentPickerUtils.logd(
"queryLinksInBackground take time: " + (SystemClock.elapsedRealtime() - start));
if (mProgressAlertDialog.isShowing()) {
mHandle.post(() -> {
synchronized (mHandle) {
if (mProgressAlertDialog.isShowing()) {
mProgressAlertDialog.dismiss();
IntentPickerUtils.logd("mProgressAlertDialog.dismiss() and isShowing: "
+ mProgressAlertDialog.isShowing());
launchSupportedLinksDialogFragment();
}
}
});
}
});
}
private void queryLinksInBackground() {
final List<String> links = IntentPickerUtils.getLinksList(mDomainVerificationManager,
mPackage, DOMAIN_STATE_NONE);
final int linksNo = links.size();
int index = 0;
mSupportedLinkWrapperList = new ArrayList<>();
for (String host : links) {
final List<DomainOwner> ownerList =
mDomainVerificationManager.getOwnersForDomain(host);
mSupportedLinkWrapperList.add(new SupportedLinkWrapper(getActivity(), host, ownerList));
index++;
// The cancel was clicked while progressing to collect data.
if (!mProgressAlertDialog.isShowing()) {
Log.w(TAG, "Exit the background thread!!!");
// clear buffer
mSupportedLinkWrapperList.clear();
break;
}
int progress = (int) (index * 100) / linksNo;
mHandle.post(() -> {
synchronized (mHandle) {
if (!mProgressAlertDialog.isShowing()) {
Log.w(TAG, "Exit the UI thread");
return;
}
mProgressAlertDialog.getProgressBar().setProgress(progress);
}
});
if (ownerList.size() == 0) {
SystemClock.sleep(PROGRESS_BAR_STEPPING_TIME);
}
}
IntentPickerUtils.logd("queryLinksInBackground : SupportedLinkWrapperList size="
+ mSupportedLinkWrapperList.size());
Collections.sort(mSupportedLinkWrapperList);
}
private void launchSupportedLinksDialogFragment() {
if (mSupportedLinkWrapperList.size() > 0) {
mViewModel.setSupportedLinkWrapperList(mSupportedLinkWrapperList);
final Bundle args = new Bundle();
args.putString(AppLaunchSettings.APP_PACKAGE_KEY, mPackage);
final SupportedLinksDialogFragment dialogFragment = new SupportedLinksDialogFragment();
dialogFragment.setArguments(args);
dialogFragment.showDialog(getActivity().getSupportFragmentManager());
}
}
/** Create a custom {@link AlertDialog} with a {@link ProgressBar}. */
static class ProgressAlertDialog extends AlertDialog {
private ProgressBar mProgressBar;
protected ProgressAlertDialog(@NonNull Context context) {
this(context, 0);
}
protected ProgressAlertDialog(@NonNull Context context, int themeResId) {
super(context, themeResId);
init(context);
}
private void init(Context context) {
final View view = LayoutInflater.from(context).inflate(
R.layout.app_launch_progress, /* root= */ null);
mProgressBar = view.findViewById(R.id.scan_links_progressbar);
mProgressBar.setProgress(0);
mProgressBar.setMax(100);
setView(view);
}
ProgressBar getProgressBar() {
return mProgressBar;
}
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.app.Application;
import androidx.lifecycle.AndroidViewModel;
import java.util.ArrayList;
import java.util.List;
/**
* This {@link AndroidViewModel} provides supported link wrapper data
* between multiple fragments.
*/
public class SupportedLinkViewModel extends AndroidViewModel {
private List<SupportedLinkWrapper> mSupportedLinkWrapperList;
public SupportedLinkViewModel(Application application) {
super(application);
}
/** Clear the list buffer of the {@link SupportedLinkWrapper}. */
public void clearSupportedLinkWrapperList() {
mSupportedLinkWrapperList = new ArrayList<>();
}
/** Set the list buffer of the {@link SupportedLinkWrapper}. */
public void setSupportedLinkWrapperList(List<SupportedLinkWrapper> wrapperList) {
mSupportedLinkWrapperList = wrapperList;
}
/** Get the list buffer of the {@link SupportedLinkWrapper}. */
public List<SupportedLinkWrapper> getSupportedLinkWrapperList() {
return mSupportedLinkWrapperList;
}
}

View File

@@ -0,0 +1,124 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainOwner;
import android.text.TextUtils;
import android.util.Log;
import com.android.settings.R;
import java.util.List;
import java.util.stream.Collectors;
/**
* A buffer of the supported link data. This {@link SupportedLinkWrapper} wraps the host, enabled
* and a list of {@link DomainOwner}.
*/
public class SupportedLinkWrapper implements Comparable {
private static final String TAG = "SupportedLinkWrapper";
private String mHost;
private List<DomainOwner> mOwnerList;
private boolean mIsEnabled;
private String mLastOwnerName;
private boolean mIsChecked;
public SupportedLinkWrapper(Context context, String host, List<DomainOwner> ownerList) {
mHost = host;
mOwnerList = ownerList;
mIsEnabled = true;
mLastOwnerName = "";
mIsChecked = false;
init(context);
}
private void init(Context context) {
if (mOwnerList.size() > 0) {
final long nonOverirideableNo = mOwnerList.stream()
.filter(it -> !it.isOverrideable())
.count();
mIsEnabled = (nonOverirideableNo == 0L);
if (nonOverirideableNo > 0L) {
mLastOwnerName = getLastPackageLabel(context, false);
} else {
mLastOwnerName = getLastPackageLabel(context, true);
}
}
}
private String getLastPackageLabel(Context context, boolean isOverrideable) {
final List<String> labelList = mOwnerList.stream()
.filter(it -> it.isOverrideable() == isOverrideable)
.map(it -> getLabel(context, it.getPackageName()))
.filter(label -> label != null)
.collect(Collectors.toList());
return labelList.get(labelList.size() - 1);
}
private String getLabel(Context context, String pkg) {
try {
final PackageManager pm = context.getPackageManager();
return pm.getApplicationInfo(pkg, /* flags= */ 0).loadLabel(pm).toString();
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "getLabel error : " + e.getMessage());
return null;
}
}
/** Returns the enabled/disabled value for list item. */
public boolean isEnabled() {
return mIsEnabled;
}
/** Returns the display format of list item in the Supported Links dialog */
public String getDisplayTitle(Context context) {
if (TextUtils.isEmpty(mLastOwnerName) || context == null) {
return mHost;
}
return mHost + System.lineSeparator() + context.getString(
R.string.app_launch_supported_links_subtext, mLastOwnerName);
}
/** Returns the host name. */
public String getHost() {
return mHost;
}
/** Returns the checked value for list item. */
public boolean isChecked() {
return mIsChecked;
}
/** Set the checked value. */
public void setChecked(boolean isChecked) {
mIsChecked = isChecked;
}
@Override
public int compareTo(Object o) {
final SupportedLinkWrapper that = (SupportedLinkWrapper) o;
if (this.mIsEnabled != that.mIsEnabled) {
return this.mIsEnabled ? -1 : 1;
}
if (TextUtils.isEmpty(this.mLastOwnerName) != TextUtils.isEmpty(that.mLastOwnerName)) {
return TextUtils.isEmpty(this.mLastOwnerName) ? -1 : 1;
}
return 0;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.CheckedTextView;
import com.android.settings.R;
import java.util.List;
/** This adapter is for supported links dialog. */
public class SupportedLinksAdapter extends BaseAdapter {
private final Context mContext;
private final List<SupportedLinkWrapper> mWrapperList;
public SupportedLinksAdapter(Context context, List<SupportedLinkWrapper> list) {
mContext = context;
mWrapperList = list;
}
@Override
public int getCount() {
return mWrapperList.size();
}
@Override
public Object getItem(int position) {
if (position < mWrapperList.size()) {
return mWrapperList.get(position);
}
return null;
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(mContext).inflate(
R.layout.supported_links_dialog_item, /* root= */ null);
}
final CheckedTextView textView = convertView.findViewById(android.R.id.text1);
textView.setText(mWrapperList.get(position).getDisplayTitle(mContext));
textView.setEnabled(mWrapperList.get(position).isEnabled());
textView.setChecked(mWrapperList.get(position).isChecked());
textView.setOnClickListener(l -> {
textView.toggle();
mWrapperList.get(position).setChecked(textView.isChecked());
});
return convertView;
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.app.Dialog;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.verify.domain.DomainVerificationManager;
import android.content.pm.verify.domain.DomainVerificationUserState;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.Log;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import com.android.settings.R;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/** A customized {@link DialogFragment} with multiple checkboxes. */
public class SupportedLinksDialogFragment extends DialogFragment {
private static final String TAG = "SupportedLinksDialogFrg";
private static final String DLG_ID = "SupportedLinksDialog";
private SupportedLinkViewModel mViewModel;
private List<SupportedLinkWrapper> mSupportedLinkWrapperList;
private String mPackage;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPackage = getArguments().getString(AppLaunchSettings.APP_PACKAGE_KEY);
mViewModel = ViewModelProviders.of(this.getActivity()).get(SupportedLinkViewModel.class);
mSupportedLinkWrapperList = mViewModel.getSupportedLinkWrapperList();
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getActivity();
final SupportedLinksAdapter adapter = new SupportedLinksAdapter(context,
mSupportedLinkWrapperList);
final AlertDialog.Builder builder = new AlertDialog
.Builder(context)
.setTitle(IntentPickerUtils.getCentralizedDialogTitle(getSupportedLinksTitle()))
.setAdapter(adapter, /* listener= */ null)
.setCancelable(true)
.setPositiveButton(R.string.app_launch_supported_links_add, (dialog, id) -> {
// addSelectedItems(((AlertDialog) dialog).getListView());
doSelectedAction();
})
.setNegativeButton(R.string.app_launch_dialog_cancel, /* listener= */ null);
return builder.create();
}
/** Display the dialog. */
public void showDialog(FragmentManager manager) {
show(manager, DLG_ID);
}
private String getSupportedLinksTitle() {
final int supportedLinksNo = mSupportedLinkWrapperList.size();
return getResources().getQuantityString(
R.plurals.app_launch_supported_links_title, supportedLinksNo, supportedLinksNo);
}
private void doSelectedAction() {
final DomainVerificationManager manager = getActivity().getSystemService(
DomainVerificationManager.class);
final DomainVerificationUserState userState =
IntentPickerUtils.getDomainVerificationUserState(manager, mPackage);
if (userState == null || mSupportedLinkWrapperList == null) {
return;
}
updateUserSelection(manager, userState);
displaySelectedItem();
}
private void updateUserSelection(DomainVerificationManager manager,
DomainVerificationUserState userState) {
final Set<String> domainSet = new ArraySet<>();
for (SupportedLinkWrapper wrapper : mSupportedLinkWrapperList) {
if (wrapper.isChecked()) {
domainSet.add(wrapper.getHost());
}
}
if (domainSet.size() > 0) {
setDomainVerificationUserSelection(manager, userState.getIdentifier(),
domainSet, /* enabled= */true);
}
}
private void setDomainVerificationUserSelection(DomainVerificationManager manager,
UUID identifier, Set<String> domainSet, boolean isEnabled) {
try {
manager.setDomainVerificationUserSelection(identifier, domainSet, isEnabled);
} catch (PackageManager.NameNotFoundException e) {
Log.w(TAG, "addSelectedItems : " + e.getMessage());
}
}
private void displaySelectedItem() {
final List<Fragment> fragments = getActivity().getSupportFragmentManager().getFragments();
for (Fragment fragment : fragments) {
if (fragment instanceof AppLaunchSettings) {
((AppLaunchSettings) fragment).addSelectedLinksPreference();
}
}
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.applications.intentpicker;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.CheckBox;
import androidx.preference.PreferenceViewHolder;
import com.android.settings.R;
import com.android.settingslib.widget.TwoTargetPreference;
/** This customized VerifiedLinksPreference was belonged to Open by default page */
public class VerifiedLinksPreference extends TwoTargetPreference {
private Context mContext;
private View.OnClickListener mOnWidgetClickListener;
private boolean mShowCheckBox;
public VerifiedLinksPreference(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
public VerifiedLinksPreference(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, /* defStyleRes= */0);
}
public VerifiedLinksPreference(Context context, AttributeSet attrs) {
this(context, attrs, /* defStyleAttr= */ 0);
}
public VerifiedLinksPreference(Context context) {
this(context, /* attrs= */ null);
}
private void init(Context context) {
mContext = context;
mOnWidgetClickListener = null;
mShowCheckBox = true;
setLayoutResource(R.layout.preference_checkable_two_target);
setWidgetLayoutResource(R.layout.verified_links_widget);
}
/**
* Register a callback to be invoked when this widget is clicked.
*
* @param listener The callback that will run.
*/
public void setWidgetFrameClickListener(View.OnClickListener listener) {
mOnWidgetClickListener = listener;
}
/** Determine the visibility of the {@link CheckBox}. */
public void setCheckBoxVisible(boolean isVisible) {
mShowCheckBox = isVisible;
}
@Override
public void onBindViewHolder(PreferenceViewHolder view) {
super.onBindViewHolder(view);
final View settingsWidget = view.findViewById(android.R.id.widget_frame);
final View divider = view.findViewById(R.id.two_target_divider);
divider.setVisibility(View.VISIBLE);
settingsWidget.setVisibility(View.VISIBLE);
if (mOnWidgetClickListener != null) {
settingsWidget.setOnClickListener(mOnWidgetClickListener);
}
final View checkboxContainer = view.findViewById(R.id.checkbox_container);
final View parentView = (View) checkboxContainer.getParent();
parentView.setEnabled(false);
parentView.setClickable(false);
CheckBox checkBox = (CheckBox) view.findViewById(com.android.internal.R.id.checkbox);
if (checkBox != null) {
checkBox.setChecked(true);
checkBox.setVisibility(mShowCheckBox ? View.VISIBLE : View.INVISIBLE);
}
}
}