Add activity to manage the list of default apps.
This change adds the activity to manage the list of default apps.
Bug: 110557011
Test: manual
Change-Id: I6d21c0296b3518ef478d189135c1b2b53cac47d3
diff --git a/PermissionController/AndroidManifest.xml b/PermissionController/AndroidManifest.xml
index aace6df..a3bcda6 100644
--- a/PermissionController/AndroidManifest.xml
+++ b/PermissionController/AndroidManifest.xml
@@ -124,6 +124,19 @@
</intent-filter>
</activity>
+ <!-- TODO: STOPSHIP: Removed permissions because apps were able to launch it. Is this good? -->
+ <activity android:name="com.android.packageinstaller.role.ui.DefaultAppListActivity"
+ android:label="@string/default_apps"
+ android:theme="@style/Settings">
+ <!-- TODO: STOPSHIP: give this a priority greater than 1 to override Settings. -->
+ <intent-filter android:priority="0">
+ <action android:name="android.settings.MANAGE_DEFAULT_APPS_SETTINGS" />
+ <!-- TODO: Redirect into role page? -->
+ <action android:name="android.settings.HOME_SETTINGS" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
<provider android:name="com.android.packageinstaller.permission.service.PermissionSearchIndexablesProvider"
android:authorities="com.android.permissioncontroller"
android:multiprocess="false"
diff --git a/PermissionController/res/layout/settings.xml b/PermissionController/res/layout/settings.xml
new file mode 100644
index 0000000..bc34f2d
--- /dev/null
+++ b/PermissionController/res/layout/settings.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2018 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:id="@+id/loading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="vertical"
+ android:visibility="invisible">
+
+ <ProgressBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="?android:progressBarStyle" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:text="@string/loading"
+ android:textAppearance="@android:style/TextAppearance.Material.Body1" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/empty"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:visibility="invisible"
+ style="@android:style/TextAppearance.Material.Subhead" />
+</FrameLayout>
diff --git a/PermissionController/res/menu/settings.xml b/PermissionController/res/menu/settings.xml
new file mode 100644
index 0000000..74c0e3f
--- /dev/null
+++ b/PermissionController/res/menu/settings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2018 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.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <item
+ android:id="@+id/action_search"
+ android:icon="@drawable/ic_search_24dp"
+ android:title="@string/search_menu"
+ android:showAsAction="always" />
+</menu>
diff --git a/PermissionController/res/values/strings.xml b/PermissionController/res/values/strings.xml
index 70ee605..c458a41 100644
--- a/PermissionController/res/values/strings.xml
+++ b/PermissionController/res/values/strings.xml
@@ -341,6 +341,16 @@
<!-- The notification content for background location access reminder notification [CHAR LIMIT=none] -->
<string name="background_location_access_reminder_notification_content">This app can always access your location. Tap to change.</string>
+ <!-- Title for page of managing default apps. [CHAR LIMIT=30] -->
+ <string name="default_apps">Default apps</string>
+
+ <!-- TODO: STOPSHIP: I cannot find its value in Settings. -->
+ <!-- Help URI, default apps [DO NOT TRANSLATE] -->
+ <string name="help_uri_default_apps" translatable="false"></string>
+
+ <!-- Label when there is no default apps [CHAR LIMIT=30] -->
+ <string name="no_default_apps">No default apps</string>
+
<!-- TODO: STOPSHIP: Migrate all default app titles from packages/apps/Settings/res/values/strings.xml . -->
<!-- Label for the dialer role. [CHAR LIMIT=30] -->
diff --git a/PermissionController/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java b/PermissionController/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java
index c81d139..af4b21b 100644
--- a/PermissionController/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java
+++ b/PermissionController/src/com/android/packageinstaller/permission/ui/handheld/PermissionsFrameFragment.java
@@ -16,17 +16,10 @@
package com.android.packageinstaller.permission.ui.handheld;
-import static android.provider.Settings.ACTION_APP_SEARCH_SETTINGS;
-import static android.view.MenuItem.SHOW_AS_ACTION_ALWAYS;
-
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
import android.os.Bundle;
-import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
-import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@@ -37,12 +30,12 @@
import androidx.preference.PreferenceFragmentCompat;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.packageinstaller.permission.utils.Utils;
import com.android.permissioncontroller.R;
public abstract class PermissionsFrameFragment extends PreferenceFragmentCompat {
private static final String LOG_TAG = PermissionsFrameFragment.class.getSimpleName();
- private static final int MENU_SEARCH_SETTINGS = Menu.FIRST;
static final int MENU_ALL_PERMS = Menu.FIRST + 1;
static final int MENU_SHOW_SYSTEM = Menu.FIRST + 2;
static final int MENU_HIDE_SYSTEM = Menu.FIRST + 3;
@@ -66,28 +59,7 @@
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
- if (getContext().getPackageManager().resolveActivity(new Intent(ACTION_APP_SEARCH_SETTINGS),
- 0) != null) {
- MenuItem searchItem = menu.add(Menu.NONE, MENU_SEARCH_SETTINGS, Menu.NONE,
- R.string.search_menu);
- searchItem.setIcon(R.drawable.ic_search_24dp);
- searchItem.setShowAsAction(SHOW_AS_ACTION_ALWAYS);
- }
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- switch (item.getItemId()) {
- case MENU_SEARCH_SETTINGS:
- try {
- getActivity().startActivity(new Intent(ACTION_APP_SEARCH_SETTINGS));
- } catch (ActivityNotFoundException e) {
- Log.e(LOG_TAG, "Cannot search settings", e);
- }
- break;
- }
-
- return super.onOptionsItemSelected(item);
+ Utils.prepareSearchMenuItem(menu, requireContext());
}
@Override
diff --git a/PermissionController/src/com/android/packageinstaller/permission/utils/Utils.java b/PermissionController/src/com/android/packageinstaller/permission/utils/Utils.java
index 85be02f..e4caec3 100644
--- a/PermissionController/src/com/android/packageinstaller/permission/utils/Utils.java
+++ b/PermissionController/src/com/android/packageinstaller/permission/utils/Utils.java
@@ -31,6 +31,7 @@
import static android.Manifest.permission_group.STORAGE;
import android.Manifest;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
@@ -42,12 +43,15 @@
import android.graphics.drawable.Drawable;
import android.os.Parcelable;
import android.os.UserHandle;
+import android.provider.Settings;
import android.text.Html;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.TypedValue;
+import android.view.Menu;
+import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -439,4 +443,28 @@
long days = hours / 24;
return context.getResources().getQuantityString(R.plurals.days, (int) days, days);
}
+
+ /**
+ * Add a menu item for searching Settings, if there is an activity handling the action.
+ *
+ * @param menu the menu to add the menu item into
+ * @param context the context for checking whether there is an activity handling the action
+ */
+ public static void prepareSearchMenuItem(@NonNull Menu menu, @NonNull Context context) {
+ Intent intent = new Intent(Settings.ACTION_APP_SEARCH_SETTINGS);
+ if (context.getPackageManager().resolveActivity(intent, 0) == null) {
+ return;
+ }
+ MenuItem searchItem = menu.add(Menu.NONE, Menu.NONE, Menu.NONE, R.string.search_menu);
+ searchItem.setIcon(R.drawable.ic_search_24dp);
+ searchItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+ searchItem.setOnMenuItemClickListener(item -> {
+ try {
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(LOG_TAG, "Cannot start activity to search settings", e);
+ }
+ return true;
+ });
+ }
}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/AppIconPreference.java b/PermissionController/src/com/android/packageinstaller/role/ui/AppIconPreference.java
new file mode 100644
index 0000000..244f4c3
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/AppIconPreference.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.Px;
+import androidx.annotation.StyleRes;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceViewHolder;
+
+import com.android.permissioncontroller.R;
+
+/**
+ * {@link Preference} with its icon view set to a fixed size for app icons.
+ */
+public class AppIconPreference extends Preference {
+
+ private Mixin mMixin;
+
+ public AppIconPreference(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ init();
+ }
+
+ public AppIconPreference(@NonNull Context context, @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+
+ init();
+ }
+
+ public AppIconPreference(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+
+ init();
+ }
+
+ public AppIconPreference(@NonNull Context context) {
+ super(context);
+
+ init();
+ }
+
+ private void init() {
+ mMixin = new Mixin(getContext());
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
+ super.onBindViewHolder(holder);
+
+ mMixin.onBindViewHolder(holder);
+ }
+
+ /**
+ * Mixin for implementation of {@link AppIconPreference}.
+ */
+ public static class Mixin {
+
+ @Px
+ private int mIconSize;
+
+ public Mixin(@NonNull Context context) {
+ mIconSize = context.getResources().getDimensionPixelSize(
+ R.dimen.secondary_app_icon_size);
+ }
+
+ /**
+ * Binds the view holder so that its icon view is set to a fixed size for app icons.
+ *
+ * @param holder the view holder passed in by {@link Preference#onBindViewHolder(
+ * PreferenceViewHolder)}
+ *
+ * @see Preference#onBindViewHolder(PreferenceViewHolder)
+ */
+ public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
+ View iconView = holder.findViewById(android.R.id.icon);
+ ViewGroup.LayoutParams layoutParams = iconView.getLayoutParams();
+ boolean changed = false;
+ if (layoutParams.width != mIconSize) {
+ layoutParams.width = mIconSize;
+ changed = true;
+ }
+ if (layoutParams.height != mIconSize) {
+ layoutParams.height = mIconSize;
+ changed = true;
+ }
+ if (changed) {
+ iconView.requestLayout();
+ }
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/AsyncTaskLiveData.java b/PermissionController/src/com/android/packageinstaller/role/ui/AsyncTaskLiveData.java
new file mode 100644
index 0000000..970bfd3
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/AsyncTaskLiveData.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.os.AsyncTask;
+
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LiveData;
+
+/**
+ * {@link LiveData} that uses {@link AsyncTask} to load value on a background thread.
+ *
+ * @param <T> type of the value
+ */
+public abstract class AsyncTaskLiveData<T> extends LiveData<T> {
+
+ /**
+ * Load the value on a background thread. The value will be reloaded even if already loaded.
+ */
+ public void loadValue() {
+ AsyncTask.execute(() -> postValue(loadValueInBackground()));
+ }
+
+ @WorkerThread
+ protected abstract T loadValueInBackground();
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListActivity.java b/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListActivity.java
new file mode 100644
index 0000000..66bee25
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListActivity.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+/**
+ * Activity for the list of default apps.
+ */
+public class DefaultAppListActivity extends FragmentActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (savedInstanceState == null) {
+ DefaultAppListFragment fragment = DefaultAppListFragment.newInstance();
+ getSupportFragmentManager().beginTransaction()
+ .add(android.R.id.content, fragment)
+ .commit();
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListFragment.java b/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListFragment.java
new file mode 100644
index 0000000..536252d
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/DefaultAppListFragment.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.preference.Preference;
+import androidx.preference.PreferenceManager;
+import androidx.preference.PreferenceScreen;
+
+import com.android.packageinstaller.permission.utils.IconDrawableFactory;
+import com.android.packageinstaller.permission.utils.Utils;
+import com.android.packageinstaller.role.model.Role;
+import com.android.permissioncontroller.R;
+
+import java.util.List;
+
+/**
+ * Fragment for the list of default apps.
+ */
+public class DefaultAppListFragment extends SettingsFragment
+ implements Preference.OnPreferenceClickListener {
+
+ private static final String LOG_TAG = DefaultAppListFragment.class.getSimpleName();
+
+ private RoleListViewModel mViewModel;
+
+ /**
+ * Create a new instance of this fragment.
+ *
+ * @return a new instance of this fragment
+ */
+ @NonNull
+ public static DefaultAppListFragment newInstance() {
+ return new DefaultAppListFragment();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ mViewModel = ViewModelProviders.of(this, new RoleListViewModel.Factory(true,
+ requireActivity().getApplication())).get(RoleListViewModel.class);
+ mViewModel.getLiveData().observe(this, this::onRoleListChanged);
+ }
+
+ @Override
+ @StringRes
+ protected int getEmptyTextResource() {
+ return R.string.no_default_apps;
+ }
+
+ @Override
+ protected int getHelpUriResource() {
+ return R.string.help_uri_default_apps;
+ }
+
+ private void onRoleListChanged(@NonNull List<RoleItem> roleItems) {
+ PreferenceManager preferenceManager = getPreferenceManager();
+ Context context = preferenceManager.getContext();
+
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ ArrayMap<String, Preference> oldPreferences = new ArrayMap<>();
+ if (preferenceScreen == null) {
+ preferenceScreen = preferenceManager.createPreferenceScreen(context);
+ setPreferenceScreen(preferenceScreen);
+ } else {
+ for (int i = preferenceScreen.getPreferenceCount() - 1; i >= 0; --i) {
+ Preference preference = preferenceScreen.getPreference(i);
+
+ preferenceScreen.removePreference(preference);
+ oldPreferences.put(preference.getKey(), preference);
+ }
+ }
+
+ int roleItemsSize = roleItems.size();
+ for (int roleItemsIndex = 0; roleItemsIndex < roleItemsSize; roleItemsIndex++) {
+ RoleItem roleItem = roleItems.get(roleItemsIndex);
+
+ List<ApplicationInfo> holderApplicationInfos = roleItem.getHolderApplicationInfos();
+ if (holderApplicationInfos.isEmpty()) {
+ // TODO: Handle Assistant which is visible even without holder.
+ continue;
+ }
+
+ Role role = roleItem.getRole();
+ Preference preference = oldPreferences.get(role.getName());
+ if (preference == null) {
+ preference = new AppIconPreference(context);
+ preference.setKey(role.getName());
+ preference.setIconSpaceReserved(true);
+ preference.setTitle(role.getLabelResource());
+ preference.setPersistent(false);
+ preference.setOnPreferenceClickListener(this);
+ }
+
+ ApplicationInfo holderApplicationInfo = holderApplicationInfos.get(0);
+ preference.setIcon(IconDrawableFactory.getBadgedIcon(context, holderApplicationInfo,
+ UserHandle.getUserHandleForUid(holderApplicationInfo.uid)));
+ preference.setSummary(Utils.getAppLabel(holderApplicationInfo, context));
+
+ // TODO: Ordering?
+ preferenceScreen.addPreference(preference);
+ }
+
+ updateState();
+ }
+
+ @Override
+ public boolean onPreferenceClick(@NonNull Preference preference) {
+ // TODO
+ return true;
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/RequestRoleFragment.java b/PermissionController/src/com/android/packageinstaller/role/ui/RequestRoleFragment.java
index 955b01d..cac4bc5 100644
--- a/PermissionController/src/com/android/packageinstaller/role/ui/RequestRoleFragment.java
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/RequestRoleFragment.java
@@ -65,12 +65,12 @@
*/
public static RequestRoleFragment newInstance(@NonNull String roleName,
@NonNull String packageName) {
- RequestRoleFragment instance = new RequestRoleFragment();
+ RequestRoleFragment fragment = new RequestRoleFragment();
Bundle arguments = new Bundle();
arguments.putString(RoleManager.EXTRA_REQUEST_ROLE_NAME, roleName);
arguments.putString(Intent.EXTRA_PACKAGE_NAME, packageName);
- instance.setArguments(arguments);
- return instance;
+ fragment.setArguments(arguments);
+ return fragment;
}
@Override
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/RoleItem.java b/PermissionController/src/com/android/packageinstaller/role/ui/RoleItem.java
new file mode 100644
index 0000000..01118e8
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/RoleItem.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.content.pm.ApplicationInfo;
+
+import androidx.annotation.NonNull;
+
+import com.android.packageinstaller.role.model.Role;
+
+import java.util.List;
+
+/**
+ * Information about a role to be displayed in a list of roles.
+ */
+public class RoleItem {
+
+ /**
+ * The {@link Role} for this role.
+ */
+ @NonNull
+ private final Role mRole;
+
+ /**
+ * The list of {@link ApplicationInfo} of applications holding this role.
+ */
+ @NonNull
+ private final List<ApplicationInfo> mHolderApplicationInfos;
+
+ public RoleItem(@NonNull Role role, @NonNull List<ApplicationInfo> holderApplicationInfos) {
+ mRole = role;
+ mHolderApplicationInfos = holderApplicationInfos;
+ }
+
+ @NonNull
+ public Role getRole() {
+ return mRole;
+ }
+
+ @NonNull
+ public List<ApplicationInfo> getHolderApplicationInfos() {
+ return mHolderApplicationInfos;
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/RoleListLiveData.java b/PermissionController/src/com/android/packageinstaller/role/ui/RoleListLiveData.java
new file mode 100644
index 0000000..af22753
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/RoleListLiveData.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.app.role.RoleManager;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.WorkerThread;
+import androidx.lifecycle.LiveData;
+
+import com.android.packageinstaller.role.model.Role;
+import com.android.packageinstaller.role.model.Roles;
+import com.android.packageinstaller.role.utils.PackageUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link LiveData} for a list of roles.
+ */
+public class RoleListLiveData extends AsyncTaskLiveData<List<RoleItem>> {
+
+ private static final String LOG_TAG = RoleListLiveData.class.getSimpleName();
+
+ private final boolean mExclusive;
+ private final Context mContext;
+
+ public RoleListLiveData(boolean exclusive, @NonNull Context context) {
+ mExclusive = exclusive;
+ mContext = context;
+
+ loadValue();
+ }
+
+ @Override
+ @WorkerThread
+ protected List<RoleItem> loadValueInBackground() {
+ ArrayMap<String, Role> roles = Roles.getRoles(mContext);
+
+ List<RoleItem> roleItems = new ArrayList<>();
+ RoleManager roleManager = mContext.getSystemService(RoleManager.class);
+ int rolesSize = roles.size();
+ for (int rolesIndex = 0; rolesIndex < rolesSize; rolesIndex++) {
+ Role role = roles.valueAt(rolesIndex);
+
+ if (role.isExclusive() != mExclusive) {
+ continue;
+ }
+
+ List<ApplicationInfo> holderApplicationInfos = new ArrayList<>();
+ List<String> holderPackageNames = roleManager.getRoleHolders(role.getName());
+ int holderPackageNamesSize = holderPackageNames.size();
+ for (int holderPackageNamesIndex = 0; holderPackageNamesIndex < holderPackageNamesSize;
+ holderPackageNamesIndex++) {
+ String holderPackageName = holderPackageNames.get(holderPackageNamesIndex);
+
+ ApplicationInfo holderApplicationInfo = PackageUtils.getApplicationInfo(
+ holderPackageName, mContext);
+ if (holderApplicationInfo == null) {
+ Log.w(LOG_TAG, "Cannot get ApplicationInfo for application, skipping: "
+ + holderPackageName);
+ continue;
+ }
+ holderApplicationInfos.add(holderApplicationInfo);
+ }
+
+ roleItems.add(new RoleItem(role, holderApplicationInfos));
+ }
+
+ return roleItems;
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/RoleListViewModel.java b/PermissionController/src/com/android/packageinstaller/role/ui/RoleListViewModel.java
new file mode 100644
index 0000000..de29928
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/RoleListViewModel.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.app.Application;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.AndroidViewModel;
+import androidx.lifecycle.ViewModel;
+import androidx.lifecycle.ViewModelProvider;
+
+/**
+ * {@link ViewModel} for a list of roles.
+ */
+public class RoleListViewModel extends AndroidViewModel {
+
+ @NonNull
+ private final RoleListLiveData mLiveData;
+
+ public RoleListViewModel(boolean exclusive, @NonNull Application application) {
+ super(application);
+
+ mLiveData = new RoleListLiveData(exclusive, application);
+ }
+
+ @NonNull
+ public RoleListLiveData getLiveData() {
+ return mLiveData;
+ }
+
+ /**
+ * {@link ViewModelProvider.Factory} for {@link RoleListViewModel}.
+ */
+ public static class Factory implements ViewModelProvider.Factory {
+
+ private boolean mExclusive;
+
+ @NonNull
+ private Application mApplication;
+
+ public Factory(boolean exclusive, @NonNull Application application) {
+ mExclusive = exclusive;
+ mApplication = application;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ //noinspection unchecked
+ return (T) new RoleListViewModel(mExclusive, mApplication);
+ }
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/ui/SettingsFragment.java b/PermissionController/src/com/android/packageinstaller/role/ui/SettingsFragment.java
new file mode 100644
index 0000000..c75bd04
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/ui/SettingsFragment.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.ui;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.preference.PreferenceFragmentCompat;
+import androidx.preference.PreferenceScreen;
+
+import com.android.packageinstaller.permission.utils.Utils;
+import com.android.packageinstaller.role.utils.UiUtils;
+import com.android.permissioncontroller.R;
+import com.android.settingslib.HelpUtils;
+
+/**
+ * Base class for settings fragments.
+ */
+public abstract class SettingsFragment extends PreferenceFragmentCompat {
+
+ private static final String LOG_TAG = SettingsFragment.class.getSimpleName();
+
+ private FrameLayout mContentLayout;
+ private LinearLayout mPreferenceLayout;
+ private View mLoadingView;
+ private TextView mEmptyText;
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ mContentLayout = (FrameLayout) inflater.inflate(R.layout.settings, container, false);
+ mPreferenceLayout = (LinearLayout) super.onCreateView(inflater, container,
+ savedInstanceState);
+ mContentLayout.addView(mPreferenceLayout);
+ return mContentLayout;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mLoadingView = mContentLayout.findViewById(R.id.loading);
+ mEmptyText = mContentLayout.findViewById(R.id.empty);
+ }
+
+ @Override
+ public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) {
+ // We'll manually add preferences later.
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+
+ requireActivity().getActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mEmptyText.setText(getEmptyTextResource());
+
+ updateState();
+ }
+
+ @StringRes
+ protected abstract int getEmptyTextResource();
+
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+
+ Utils.prepareSearchMenuItem(menu, requireContext());
+ int helpUriResource = getHelpUriResource();
+ if (helpUriResource != 0) {
+ HelpUtils.prepareHelpMenuItem(requireActivity(), menu, helpUriResource,
+ getClass().getName());
+ }
+ }
+
+ @StringRes
+ protected int getHelpUriResource() {
+ return 0;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ requireActivity().finish();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ protected void updateState() {
+ PreferenceScreen preferenceScreen = getPreferenceScreen();
+ boolean isLoading = preferenceScreen == null;
+ UiUtils.setViewShown(mLoadingView, isLoading);
+ boolean isEmpty = preferenceScreen != null && preferenceScreen.getPreferenceCount() == 0;
+ UiUtils.setViewShown(mEmptyText, isEmpty);
+ }
+}
diff --git a/PermissionController/src/com/android/packageinstaller/role/utils/UiUtils.java b/PermissionController/src/com/android/packageinstaller/role/utils/UiUtils.java
new file mode 100644
index 0000000..ded3e7f
--- /dev/null
+++ b/PermissionController/src/com/android/packageinstaller/role/utils/UiUtils.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 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.packageinstaller.role.utils;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.view.View;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility methods about UI.
+ */
+public class UiUtils {
+
+ private UiUtils() {}
+
+ /**
+ * Set whether a view is shown.
+ *
+ * @param view the view to be set to shown or not
+ * @param shown whether the view should be shown
+ */
+ public static void setViewShown(@NonNull View view, boolean shown) {
+ if (shown && view.getVisibility() == View.VISIBLE && view.getAlpha() == 1) {
+ // This cancels any on-going animation.
+ view.animate()
+ .alpha(1)
+ .setDuration(0);
+ return;
+ } else if (!shown && (view.getVisibility() != View.VISIBLE || view.getAlpha() == 0)) {
+ // This cancels any on-going animation.
+ view.animate()
+ .alpha(0)
+ .setDuration(0);
+ view.setVisibility(View.INVISIBLE);
+ return;
+ }
+ if (shown && view.getVisibility() != View.VISIBLE) {
+ view.setAlpha(0);
+ view.setVisibility(View.VISIBLE);
+ }
+ int duration = view.getResources().getInteger(android.R.integer.config_mediumAnimTime);
+ Interpolator interpolator = AnimationUtils.loadInterpolator(view.getContext(), shown
+ ? android.R.interpolator.fast_out_slow_in
+ : android.R.interpolator.fast_out_linear_in);
+ view.animate()
+ .alpha(shown ? 1 : 0)
+ .setDuration(duration)
+ .setInterpolator(interpolator)
+ // Always update the listener or the view will try to reuse the previous one.
+ .setListener(shown ? null : new AnimatorListenerAdapter() {
+ private boolean mCanceled = false;
+ @Override
+ public void onAnimationCancel(@NonNull Animator animator) {
+ mCanceled = true;
+ }
+ @Override
+ public void onAnimationEnd(@NonNull Animator animator) {
+ if (!mCanceled) {
+ view.setVisibility(View.INVISIBLE);
+ }
+ }
+ });
+ }
+}