diff options
Diffstat (limited to 'java')
18 files changed, 861 insertions, 203 deletions
diff --git a/java/res/layout/chooser_grid_scrollable_preview.xml b/java/res/layout/chooser_grid_scrollable_preview.xml index a5ac75a2..c1bcf912 100644 --- a/java/res/layout/chooser_grid_scrollable_preview.xml +++ b/java/res/layout/chooser_grid_scrollable_preview.xml @@ -84,8 +84,7 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:orientation="vertical" - android:background="#f00"> + android:orientation="vertical"> <FrameLayout android:id="@androidprv:id/content_preview_container" diff --git a/java/res/values-my/strings.xml b/java/res/values-my/strings.xml index d1500aa5..c3ab1ee2 100644 --- a/java/res/values-my/strings.xml +++ b/java/res/values-my/strings.xml @@ -54,7 +54,7 @@ <string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ကို ပင်ဖြုတ်ရန်"</string> <string name="screenshot_edit" msgid="3857183660047569146">"တည်းဖြတ်ရန်"</string> <string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ဖိုင်}other{+ # ဖိုင်}}"</string> - <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ နောက်ထပ် # ဖိုင်}other{+ နောက်ထပ် # ဖိုင်}}"</string> + <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+နောက်ထပ် # ဖိုင်}other{+နောက်ထပ် # ဖိုင်}}"</string> <string name="sharing_text" msgid="8137537443603304062">"စာသား မျှဝေနေသည်"</string> <string name="sharing_link" msgid="2307694372813942916">"လင့်ခ် မျှဝေနေသည်"</string> <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ပုံ မျှဝေနေသည်}other{ပုံ # ပုံ မျှဝေနေသည်}}"</string> diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 50ca5d0d..9000ab3a 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1251,7 +1251,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements getEventLog(), maxTargetsPerRow, initialIntentsUserSpace, - targetDataLoader); + targetDataLoader, + null); } @Override diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index 3af8a3a7..876ad5c3 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -30,7 +30,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.AsyncTask; import android.os.Trace; import android.os.UserHandle; @@ -68,6 +67,17 @@ import java.util.concurrent.Executor; import java.util.stream.Collectors; public class ChooserListAdapter extends ResolverListAdapter { + + /** + * Delegate interface for injecting a chooser-specific operation to be performed before handling + * a package-change event. This allows the "driver" invoking the package-change to be generic, + * with no knowledge specific to the chooser implementation. + */ + public interface PackageChangeCallback { + /** Perform any steps necessary before processing the package-change event. */ + void beforeHandlingPackagesChanged(); + } + private static final String TAG = "ChooserListAdapter"; private static final boolean DEBUG = false; @@ -93,6 +103,9 @@ public class ChooserListAdapter extends ResolverListAdapter { private final Set<TargetInfo> mRequestedIcons = new HashSet<>(); + @Nullable + private final PackageChangeCallback mPackageChangeCallback; + // Reserve spots for incoming direct share targets by adding placeholders private final TargetInfo mPlaceHolderTargetInfo; private final TargetDataLoader mTargetDataLoader; @@ -152,7 +165,8 @@ public class ChooserListAdapter extends ResolverListAdapter { EventLog eventLog, int maxRankedTargets, UserHandle initialIntentsUserSpace, - TargetDataLoader targetDataLoader) { + TargetDataLoader targetDataLoader, + @Nullable PackageChangeCallback packageChangeCallback) { this( context, payloadIntents, @@ -169,6 +183,7 @@ public class ChooserListAdapter extends ResolverListAdapter { maxRankedTargets, initialIntentsUserSpace, targetDataLoader, + packageChangeCallback, AsyncTask.SERIAL_EXECUTOR, context.getMainExecutor()); } @@ -190,6 +205,7 @@ public class ChooserListAdapter extends ResolverListAdapter { int maxRankedTargets, UserHandle initialIntentsUserSpace, TargetDataLoader targetDataLoader, + @Nullable PackageChangeCallback packageChangeCallback, Executor bgExecutor, Executor mainExecutor) { // Don't send the initial intents through the shared ResolverActivity path, @@ -214,6 +230,7 @@ public class ChooserListAdapter extends ResolverListAdapter { mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context); mTargetDataLoader = targetDataLoader; + mPackageChangeCallback = packageChangeCallback; createPlaceHolders(); mEventLog = eventLog; mShortcutSelectionLogic = new ShortcutSelectionLogic( @@ -286,6 +303,9 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override public void handlePackagesChanged() { + if (mPackageChangeCallback != null) { + mPackageChangeCallback.beforeHandlingPackagesChanged(); + } if (DEBUG) { Log.d(TAG, "clearing queryTargets on package change"); } diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 19032b31..70812642 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -236,21 +236,20 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private final AtomicLong mIntentReceivedTime = new AtomicLong(-1); - public ChooserActivity() { - super(); - mLogic = new ChooserActivityLogic( + @Override + protected void onCreate(Bundle savedInstanceState) { + Tracer.INSTANCE.markLaunched(); + super.onCreate(savedInstanceState); + setLogic(new ChooserActivityLogic( TAG, () -> this, this::onWorkProfileStatusUpdated, () -> mTargetDataLoader, - this::onPreinitialization - ); + this::onPreinitialization)); + addInitializer(this::init); } - @Override - protected void onCreate(Bundle savedInstanceState) { - Tracer.INSTANCE.markLaunched(); - super.onCreate(savedInstanceState); + private void init() { if (getChooserRequest() == null) { finish(); return; @@ -594,25 +593,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements // Refresh pinned items mPinnedSharedPrefs = getPinnedSharedPrefs(this); if (listAdapter == null) { - handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter()); - if (mChooserMultiProfilePagerAdapter.getCount() > 1) { - handlePackageChangePerProfile( - mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); - } + mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs(); } else { - handlePackageChangePerProfile(listAdapter); + listAdapter.handlePackagesChanged(); } updateProfileViewButton(); } - private void handlePackageChangePerProfile(ResolverListAdapter adapter) { - ProfileRecord record = getProfileRecord(adapter.getUserHandle()); - if (record != null && record.shortcutLoader != null) { - record.shortcutLoader.reset(); - } - adapter.handlePackagesChanged(); - } - @Override protected void onResume() { super.onResume(); @@ -719,7 +706,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Override protected void onStop() { super.onStop(); - mRefinementManager.onActivityStop(isChangingConfigurations()); + if (mRefinementManager != null) { + mRefinementManager.onActivityStop(isChangingConfigurations()); + } if (mFinishWhenStopped) { mFinishWhenStopped = false; @@ -1268,7 +1257,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements getEventLog(), maxTargetsPerRow, initialIntentsUserSpace, - targetDataLoader); + targetDataLoader, + () -> { + ProfileRecord record = getProfileRecord(userHandle); + if (record != null && record.shortcutLoader != null) { + record.shortcutLoader.reset(); + } + }); } @Override @@ -1517,12 +1512,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements return PROFILE_PERSONAL; } - @Override // ResolverListCommunicator - public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { - mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged(); - super.onHandlePackagesChanged(listAdapter); - } - @Override protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { setupScrollListener(); diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt index 5303a7e7..7bc39a24 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt +++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt @@ -10,6 +10,8 @@ import com.android.intentresolver.R import com.android.intentresolver.icons.TargetDataLoader import com.android.intentresolver.v2.util.mutableLazy +private const val TAG = "ChooserActivityLogic" + /** * Activity logic for [ChooserActivity]. * diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java index 7ea78d14..de0a9426 100644 --- a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java @@ -154,6 +154,14 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter< } @Override + public boolean onHandlePackagesChanged( + ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { + // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? + getActiveListAdapter().notifyDataSetChanged(); + return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); + } + + @Override protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { if (doPostProcessing) { Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java index 212bf3b4..f785c11c 100644 --- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java @@ -353,6 +353,66 @@ public class MultiProfilePagerAdapter< return getListViewForIndex(1 - getCurrentPage()); } + private boolean anyAdapterHasItems() { + for (int i = 0; i < mItems.size(); ++i) { + ListAdapterT listAdapter = mListAdapterExtractor.apply(getAdapterForIndex(i)); + if (listAdapter.getCount() > 0) { + return true; + } + } + return false; + } + + public void refreshPackagesInAllTabs() { + // TODO: handle all inactive profiles; for now we can only have at most one. It's unclear if + // this legacy logic really requires the active tab to be rebuilt first, or if we could just + // iterate over the tabs in arbitrary order. + getActiveListAdapter().handlePackagesChanged(); + if (getCount() > 1) { + getInactiveListAdapter().handlePackagesChanged(); + } + } + + /** + * Notify that there has been a package change which could potentially modify the set of targets + * that should be shown in the specified {@code listAdapter}. This <em>may</em> result in + * "rebuilding" the target list for that adapter. + * + * @param listAdapter an adapter that may need to be updated after the package-change event. + * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet + * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any + * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild + * will be prompted when we eventually get the broadcast. + * + * @return whether we're able to proceed with a Sharesheet session after processing this + * package-change event. If false, we were able to rebuild the targets but determined that there + * aren't any we could present in the UI without the app looking broken, so we should just quit. + */ + public boolean onHandlePackagesChanged( + ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { + if (listAdapter == getActiveListAdapter()) { + if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) + && waitingToEnableWorkProfile) { + // We have just turned on the work profile and entered the passcode to start it, + // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no + // point in reloading the list now, since the work profile user is still turning on. + return true; + } + + boolean listRebuilt = rebuildActiveTab(true); + if (listRebuilt) { + listAdapter.notifyDataSetChanged(); + } + + // TODO: shouldn't we check that the inactive tabs are built before declaring that we + // have to quit for lack of items? + return anyAdapterHasItems(); + } else { + clearInactiveProfileCache(); + return true; + } + } + /** * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. */ diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 2c1497f0..3d08735e 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -20,11 +20,6 @@ import static android.Manifest.permission.INTERACT_ACROSS_PROFILES; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB; -import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.content.PermissionChecker.PID_UNKNOWN; import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL; @@ -33,7 +28,9 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; +import static java.util.Collections.emptyList; import static java.util.Objects.requireNonNull; +import static java.util.Objects.requireNonNullElse; import android.app.ActivityManager; import android.app.ActivityThread; @@ -65,7 +62,6 @@ import android.os.StrictMode; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; -import android.provider.MediaStore; import android.provider.Settings; import android.stats.devicepolicy.DevicePolicyEnums; import android.text.TextUtils; @@ -91,6 +87,7 @@ import android.widget.TabWidget; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.UiThread; @@ -113,10 +110,12 @@ import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.v2.MultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; import com.android.intentresolver.v2.MultiProfilePagerAdapter.Profile; +import com.android.intentresolver.v2.data.repository.DevicePolicyResources; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider; +import com.android.intentresolver.v2.ui.ActionTitle; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; @@ -143,11 +142,11 @@ import java.util.Set; public class ResolverActivity extends FragmentActivity implements ResolverListAdapter.ResolverListCommunicator { - protected ActivityLogic mLogic = new ResolverActivityLogic( - TAG, - () -> this, - this::onWorkProfileStatusUpdated - ); + private final List<Runnable> mInit = new ArrayList<>(); + + protected ActivityLogic mLogic; + + private DevicePolicyResources mDevicePolicyResources; public ResolverActivity() { mIsIntentPicker = getClass().equals(ResolverActivity.class); @@ -229,68 +228,6 @@ public class ResolverActivity extends FragmentActivity implements protected final LatencyTracker mLatencyTracker = getLatencyTracker(); - private enum ActionTitle { - VIEW(Intent.ACTION_VIEW, - R.string.whichViewApplication, - R.string.whichViewApplicationNamed, - R.string.whichViewApplicationLabel), - EDIT(Intent.ACTION_EDIT, - R.string.whichEditApplication, - R.string.whichEditApplicationNamed, - R.string.whichEditApplicationLabel), - SEND(Intent.ACTION_SEND, - R.string.whichSendApplication, - R.string.whichSendApplicationNamed, - R.string.whichSendApplicationLabel), - SENDTO(Intent.ACTION_SENDTO, - R.string.whichSendToApplication, - R.string.whichSendToApplicationNamed, - R.string.whichSendToApplicationLabel), - SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, - R.string.whichSendApplication, - R.string.whichSendApplicationNamed, - R.string.whichSendApplicationLabel), - CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, - R.string.whichImageCaptureApplication, - R.string.whichImageCaptureApplicationNamed, - R.string.whichImageCaptureApplicationLabel), - DEFAULT(null, - R.string.whichApplication, - R.string.whichApplicationNamed, - R.string.whichApplicationLabel), - HOME(Intent.ACTION_MAIN, - R.string.whichHomeApplication, - R.string.whichHomeApplicationNamed, - R.string.whichHomeApplicationLabel); - - // titles for layout that deals with http(s) intents - public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; - public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; - public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; - public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; - - public final String action; - public final int titleRes; - public final int namedTitleRes; - public final @StringRes int labelRes; - - ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { - this.action = action; - this.titleRes = titleRes; - this.namedTitleRes = namedTitleRes; - this.labelRes = labelRes; - } - - public static ActionTitle forAction(String action) { - for (ActionTitle title : values()) { - if (title != HOME && action != null && action.equals(title.action)) { - return title; - } - } - return DEFAULT; - } - } - protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { return new PackageMonitor() { @Override @@ -307,30 +244,58 @@ public class ResolverActivity extends FragmentActivity implements } }; } + protected interface Initializer { + void initialize(ActivityLogic value); + } + + protected void setLogic(ActivityLogic logic) { + mLogic = logic; + } + + protected void addInitializer(Runnable initializer) { + mInit.add(initializer); + } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (isFinishing()) { // Performing a clean exit: - // Skip initializing any additional resources. + // Skip initializing anything. return; } + mDevicePolicyResources = new DevicePolicyResources(getApplication().getResources(), + requireNonNull(getSystemService(DevicePolicyManager.class))); + setLogic(new ResolverActivityLogic( + TAG, + () -> this, + this::onWorkProfileStatusUpdated)); + addInitializer(this::init); + } + + @Override + protected final void onPostCreate(@Nullable Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + mInit.forEach(Runnable::run); + + if (savedInstanceState != null) { + resetButtonBar(); + ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); + if (viewPager != null) { + viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); + } + mMultiProfilePagerAdapter.clearInactiveProfileCache(); + } + } + + private void init() { setTheme(mLogic.getThemeResId()); mLogic.preInitialization(); - init( - mLogic.getTargetIntent(), - mLogic.getInitialIntents() == null - ? null : mLogic.getInitialIntents().toArray(new Intent[0]), - mLogic.getTargetDataLoader() - ); - } - protected void init( - Intent intent, - Intent[] initialIntents, - TargetDataLoader targetDataLoader - ) { + Intent intent = mLogic.getTargetIntent(); + List<Intent> initialIntents = mLogic.getInitialIntents(); + TargetDataLoader targetDataLoader = mLogic.getTargetDataLoader(); + // Calling UID did not have valid permissions if (mLogic.getAnnotatedUserHandles() == null) { finish(); @@ -350,7 +315,7 @@ public class ResolverActivity extends FragmentActivity implements boolean filterLastUsed = mLogic.getSupportsAlwaysUseOption() && !isVoiceInteraction() && !shouldShowTabs() && !hasCloneProfile(); mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( - initialIntents, + requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]), /* resolutionList = */ null, filterLastUsed, targetDataLoader @@ -606,9 +571,9 @@ public class ResolverActivity extends FragmentActivity implements ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter() .resolveInfoForPosition(which, hasIndexBeenFiltered); if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) { + String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString(); Toast.makeText(this, - getWorkProfileNotSupportedMsg( - ri.activityInfo.loadLabel(getPackageManager()).toString()), + mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName), Toast.LENGTH_LONG).show(); return; } @@ -945,29 +910,12 @@ public class ResolverActivity extends FragmentActivity implements } @Override // ResolverListCommunicator - public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { - if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) { - if (listAdapter.getUserHandle().equals( - requireAnnotatedUserHandles().workProfileUserHandle) - && mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile()) { - // We have just turned on the work profile and entered the pass code to start it, - // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no - // point in reloading the list now, since the work profile user is still - // turning on. - return; - } - boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true); - if (listRebuilt) { - ResolverListAdapter activeListAdapter = - mMultiProfilePagerAdapter.getActiveListAdapter(); - activeListAdapter.notifyDataSetChanged(); - if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) { - // We no longer have any items... just finish the activity. - finish(); - } - } - } else { - mMultiProfilePagerAdapter.clearInactiveProfileCache(); + public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) { + if (!mMultiProfilePagerAdapter.onHandlePackagesChanged( + listAdapter, + mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile())) { + // We no longer have any items... just finish the activity. + finish(); } } @@ -1363,7 +1311,7 @@ public class ResolverActivity extends FragmentActivity implements } @Override - protected final void onSaveInstanceState(Bundle outState) { + protected final void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); if (viewPager != null) { @@ -1371,17 +1319,6 @@ public class ResolverActivity extends FragmentActivity implements } } - @Override - protected final void onRestoreInstanceState(Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - resetButtonBar(); - ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); - if (viewPager != null) { - viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY)); - } - mMultiProfilePagerAdapter.clearInactiveProfileCache(); - } - private boolean hasManagedProfile() { UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); if (userManager == null) { @@ -1463,15 +1400,6 @@ public class ResolverActivity extends FragmentActivity implements mAlwaysButton.setEnabled(enabled); } - private String getWorkProfileNotSupportedMsg(String launcherName) { - return getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_WORK_PROFILE_NOT_SUPPORTED, - () -> getString( - R.string.activity_resolver_work_profiles_support, - launcherName), - launcherName); - } - @Override // ResolverListCommunicator public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, boolean rebuildCompleted) { @@ -1867,8 +1795,9 @@ public class ResolverActivity extends FragmentActivity implements Button personalButton = (Button) getLayoutInflater().inflate( R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false); - personalButton.setText(getPersonalTabLabel()); - personalButton.setContentDescription(getPersonalTabAccessibilityLabel()); + personalButton.setText(mDevicePolicyResources.getPersonalTabLabel()); + personalButton.setContentDescription( + mDevicePolicyResources.getPersonalTabAccessibilityLabel()); TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL) .setContent(com.android.internal.R.id.profile_pager) @@ -1877,8 +1806,8 @@ public class ResolverActivity extends FragmentActivity implements Button workButton = (Button) getLayoutInflater().inflate( R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false); - workButton.setText(getWorkTabLabel()); - workButton.setContentDescription(getWorkTabAccessibilityLabel()); + workButton.setText(mDevicePolicyResources.getWorkTabLabel()); + workButton.setContentDescription(mDevicePolicyResources.getWorkTabAccessibilityLabel()); tabSpec = tabHost.newTabSpec(TAB_TAG_WORK) .setContent(com.android.internal.R.id.profile_pager) @@ -1930,16 +1859,6 @@ public class ResolverActivity extends FragmentActivity implements }; } - private String getPersonalTabLabel() { - return getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_PERSONAL_TAB, () -> getString(R.string.resolver_personal_tab)); - } - - private String getWorkTabLabel() { - return getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_WORK_TAB, () -> getString(R.string.resolver_work_tab)); - } - private void maybeHideDivider() { if (!mIsIntentPicker) { return; @@ -1960,18 +1879,6 @@ public class ResolverActivity extends FragmentActivity implements .clearCheckedItemsInInactiveProfiles(); } - private String getPersonalTabAccessibilityLabel() { - return getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_PERSONAL_TAB_ACCESSIBILITY, - () -> getString(R.string.resolver_personal_tab_accessibility)); - } - - private String getWorkTabAccessibilityLabel() { - return getSystemService(DevicePolicyManager.class).getResources().getString( - RESOLVER_WORK_TAB_ACCESSIBILITY, - () -> getString(R.string.resolver_work_tab_accessibility)); - } - private static int getAttrColor(Context context, int attr) { TypedArray ta = context.obtainStyledAttributes(new int[]{attr}); int colorAccent = ta.getColor(0, 0); @@ -2097,13 +2004,6 @@ public class ResolverActivity extends FragmentActivity implements mRetainInOnStop = retainInOnStop; } - private boolean inactiveListAdapterHasItems() { - if (!shouldShowTabs()) { - return false; - } - return mMultiProfilePagerAdapter.getInactiveListAdapter().getCount() > 0; - } - final class ItemClickListener implements AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { @Override diff --git a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt new file mode 100644 index 00000000..7debdf07 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.data.repository + +import android.app.admin.DevicePolicyManager +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB +import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY +import android.content.res.Resources +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DevicePolicyResources @Inject constructor( + @ApplicationOwned private val resources: Resources, + devicePolicyManager: DevicePolicyManager +) { + private val policyResources = devicePolicyManager.resources + + val personalTabLabel by lazy { + requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB) { + resources.getString(R.string.resolver_personal_tab) + }) + } + + val workTabLabel by lazy { + requireNotNull(policyResources.getString(RESOLVER_WORK_TAB) { + resources.getString(R.string.resolver_work_tab) + }) + } + + val personalTabAccessibilityLabel by lazy { + requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB_ACCESSIBILITY) { + resources.getString(R.string.resolver_personal_tab_accessibility) + }) + } + + val workTabAccessibilityLabel by lazy { + requireNotNull(policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) { + resources.getString(R.string.resolver_work_tab_accessibility) + }) + } + + fun getWorkProfileNotSupportedMessage(launcherName: String): String { + return requireNotNull(policyResources.getString(RESOLVER_WORK_PROFILE_NOT_SUPPORTED, { + resources.getString( + R.string.activity_resolver_work_profiles_support, + launcherName) + }, launcherName)) + } +}
\ No newline at end of file diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java new file mode 100644 index 00000000..271c6f38 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.ui; + +import android.content.Intent; +import android.provider.MediaStore; + +import androidx.annotation.StringRes; + +import com.android.intentresolver.R; +import com.android.intentresolver.v2.ResolverActivity; + +/** + * Provides a set of related resources for different use cases. + */ +public enum ActionTitle { + VIEW(Intent.ACTION_VIEW, + R.string.whichViewApplication, + R.string.whichViewApplicationNamed, + R.string.whichViewApplicationLabel), + EDIT(Intent.ACTION_EDIT, + R.string.whichEditApplication, + R.string.whichEditApplicationNamed, + R.string.whichEditApplicationLabel), + SEND(Intent.ACTION_SEND, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), + SENDTO(Intent.ACTION_SENDTO, + R.string.whichSendToApplication, + R.string.whichSendToApplicationNamed, + R.string.whichSendToApplicationLabel), + SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), + CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, + R.string.whichImageCaptureApplication, + R.string.whichImageCaptureApplicationNamed, + R.string.whichImageCaptureApplicationLabel), + DEFAULT(null, + R.string.whichApplication, + R.string.whichApplicationNamed, + R.string.whichApplicationLabel), + HOME(Intent.ACTION_MAIN, + R.string.whichHomeApplication, + R.string.whichHomeApplicationNamed, + R.string.whichHomeApplicationLabel); + + // titles for layout that deals with http(s) intents + public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; + public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; + public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; + public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; + + public final String action; + public final int titleRes; + public final int namedTitleRes; + public final @StringRes int labelRes; + + ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { + this.action = action; + this.titleRes = titleRes; + this.namedTitleRes = namedTitleRes; + this.labelRes = labelRes; + } + + public static ActionTitle forAction(String action) { + for (ActionTitle title : values()) { + if (title != HOME && action != null && action.equals(title.action)) { + return title; + } + } + return DEFAULT; + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt new file mode 100644 index 00000000..9a3cc9c7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation + +import android.util.Log +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import kotlin.reflect.KClass + +sealed interface Finding { + val importance: Importance + val message: String +} + +enum class Importance { + CRITICAL, + WARNING, +} + +val Finding.logcatPriority + get() = + when (importance) { + CRITICAL -> Log.ERROR + else -> Log.WARN + } + +private fun formatMessage(key: String? = null, msg: String) = buildString { + key?.also { append("['$key']: ") } + append(msg) +} + +data class IgnoredValue( + val key: String, + val reason: String, +) : Finding { + override val importance = WARNING + + override val message: String + get() = formatMessage(key, "Ignored. $reason") +} + +data class RequiredValueMissing( + val key: String, + val allowedType: KClass<*>, +) : Finding { + + override val importance = CRITICAL + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedType.simpleName}, " + "but no value was present" + ) +} + +data class WrongElementType( + val key: String, + override val importance: Importance, + val container: KClass<*>, + val actualType: KClass<*>, + val expectedType: KClass<*> +) : Finding { + override val message: String + get() = + formatMessage( + key, + "${container.simpleName} expected with elements of " + + "${expectedType.simpleName} " + + "but found ${actualType.simpleName} values instead" + ) +} + +data class ValueIsWrongType( + val key: String, + override val importance: Importance, + val actualType: KClass<*>, + val allowedTypes: List<KClass<*>>, +) : Finding { + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + + "but was ${actualType.simpleName}" + ) +} + +data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { + override val importance: Importance + get() = CRITICAL + override val message: String + get() = + formatMessage( + key, + "An unhandled exception was caught during validation: " + + thrown.stackTraceToString() + ) +} diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt new file mode 100644 index 00000000..46939602 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation + +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING + +/** + * Provides a mechanism for validating a result from a set of properties. + * + * The results of validation are provided as [findings]. + */ +interface Validation { + val findings: List<Finding> + + /** + * Require a valid property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T + + /** + * Request an optional value for a property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + fun <T> optional(property: Validator<T>): T? + + /** + * Report a property as __ignored__. + * + * The presence of any value will report a warning citing [reason]. + */ + fun <T> ignored(property: Validator<T>, reason: String) +} + +/** Performs validation for a specific key -> value pair. */ +interface Validator<T> { + val key: String + + /** + * Performs validation on a specific value from [source]. + * + * @param source a source for reading the property value. Values are intentionally untyped + * (Any?) to avoid upstream code from making type assertions through type inference. Types are + * asserted later using a [Validator]. + * @param importance the importance of any findings + */ + fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> +} + +internal class InvalidResultError internal constructor() : Error() + +/** + * Perform a number of validations on the source, assembling and returning a Result. + * + * When an exception is thrown by [validate], it is caught here. In response, a failed + * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. + * + * @param validate perform validations and return a [ValidationResult] + */ +fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> { + val validation = ValidationImpl(source) + return runCatching { validate(validation) } + .fold( + onSuccess = { result -> Valid(result, validation.findings) }, + onFailure = { + when (it) { + // A validator has interrupted validation. Return the findings. + is InvalidResultError -> Invalid(validation.findings) + + // Some other exception was thrown from [validate], + else -> Invalid(findings = listOf(UncaughtException(it))) + } + } + ) +} + +private class ValidationImpl(val source: (String) -> Any?) : Validation { + override val findings = mutableListOf<Finding>() + + override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING) + + override fun <T> required(property: Validator<T>): T { + return validate(property, CRITICAL) ?: throw InvalidResultError() + } + + override fun <T> ignored(property: Validator<T>, reason: String) { + val result = property.validate(source, WARNING) + if (result.value != null) { + // Note: Any findings about the value (result.findings) are ignored. + findings += IgnoredValue(property.key, reason) + } + } + + private fun <T> validate(property: Validator<T>, importance: Importance): T? { + return runCatching { property.validate(source, importance) } + .fold( + onSuccess = { result -> + findings += result.findings + result.value + }, + onFailure = { + findings += UncaughtException(it, property.key) + null + } + ) + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt new file mode 100644 index 00000000..092cabe8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation + +import android.util.Log + +sealed interface ValidationResult<T> { + val value: T? + val findings: List<Finding> + + fun isSuccess() = value != null + + fun getOrThrow(): T = + checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } + + fun <T> reportToLogcat(tag: String) { + findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } + } +} + +data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) : + ValidationResult<T> + +data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> { + override val value: T? = null +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt new file mode 100644 index 00000000..3cefeb15 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation.types + +import android.content.Intent +import android.net.Uri +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType + +class IntentOrUri(override val key: String) : Validator<Intent> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<Intent> { + + return when (val value = source(key)) { + // An intent, return it. + is Intent -> Valid(value) + + // A Uri was supplied. + // Unfortunately, converting Uri -> Intent requires a toString(). + is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) + + // No value present. + null -> createResult(importance, RequiredValueMissing(key, Intent::class)) + + // Some other type. + else -> { + return createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt new file mode 100644 index 00000000..c6c4abba --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.android.intentresolver.v2.validation.WrongElementType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class ParceledArray<T : Any>( + override val key: String, + private val elementType: KClass<T>, +) : Validator<List<T>> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<List<T>> { + + return when (val value: Any? = source(key)) { + // No value present. + null -> createResult(importance, RequiredValueMissing(key, elementType)) + + // A parcel does not transfer the element type information for parcelable + // arrays. This leads to a restored type of Array<Parcelable>, which is + // incompatible with Array<T : Parcelable>. + + // To handle this safely, treat as Array<*>, assert contents of the expected + // parcelable type, and return as a list. + + is Array<*> -> { + val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } + when (invalid) { + // No invalid elements, result is ok. + null -> Valid(value.map { elementType.cast(it) }) + + // At least one incorrect element type found. + else -> + createResult( + importance, + WrongElementType( + key, + importance, + actualType = invalid::class, + container = Array::class, + expectedType = elementType + ) + ) + } + } + + // The value is not an Array at all. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(elementType) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt new file mode 100644 index 00000000..3287b84b --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class SimpleValue<T : Any>( + override val key: String, + private val expected: KClass<T>, +) : Validator<T> { + + override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> { + val value: Any? = source(key) + return when { + // The value is present and of the expected type. + expected.isInstance(value) -> return Valid(expected.cast(value)) + + // No value is present. + value == null -> createResult(importance, RequiredValueMissing(key, expected)) + + // The value is some other type. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(expected) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt new file mode 100644 index 00000000..4e6e5dff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 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.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Finding +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator + +inline fun <reified T : Any> value(key: String): Validator<T> { + return SimpleValue(key, T::class) +} + +inline fun <reified T : Any> array(key: String): Validator<List<T>> { + return ParceledArray(key, T::class) +} + +/** + * Convenience function to wrap a finding in an appropriate result type. + * + * An error [finding] is suppressed when [importance] == [WARNING] + */ +internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> { + return when (importance) { + WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING }) + CRITICAL -> Invalid(listOf(finding)) + } +} |