summaryrefslogtreecommitdiff
path: root/java/src/com
diff options
context:
space:
mode:
author Mark Renouf <mrenouf@google.com> 2023-09-11 10:53:02 -0400
committer Mark Renouf <mrenouf@google.com> 2023-10-10 15:51:55 -0400
commit35018884b00755252f5268507065a7ea6cb9404b (patch)
tree642993f8396e95e561a58b6b9022a2614ea24122 /java/src/com
parent73bad17b4fa5a7c07409ed6d46121614b48adb33 (diff)
Inject ComponentNames for image editor and nearby share
Adds a SecureSettings fake Adds test coverage for new modules Removes another overload from ChooserActivityWrapper Uses @BindValue in tests to alter the configured editor component Test: atest --test-mapping packages/modules/IntentResolver Bug: 300157408 Bug: 302113519 Change-Id: Ie7d5fe12ad0d8e7fd074154641de35fe89d50ce6
Diffstat (limited to 'java/src/com')
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java11
-rw-r--r--java/src/com/android/intentresolver/inject/SingletonModule.kt9
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActionFactory.java392
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java30
-rw-r--r--java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt35
-rw-r--r--java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt32
-rw-r--r--java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt30
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettings.kt25
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt14
9 files changed, 551 insertions, 27 deletions
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 6d56146d..c7c0beeb 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -103,7 +103,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
/**
* @param context
* @param chooserRequest data about the invocation of the current Sharesheet session.
- * @param integratedDeviceComponents info about other components that are available on this
* device to implement the supported action types.
* @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
* setting is updated. The argument is whether the shared text is to be excluded.
@@ -239,7 +238,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
clipData = extractTextToCopy(targetIntent);
} catch (Throwable t) {
Log.e(TAG, "Failed to extract data to copy", t);
- return null;
+ return null;
}
if (clipData == null) {
return null;
@@ -372,10 +371,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
null,
null,
ActivityOptions.makeCustomAnimation(
- context,
- R.anim.slide_in_right,
- R.anim.slide_out_left)
- .toBundle());
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
}
diff --git a/java/src/com/android/intentresolver/inject/SingletonModule.kt b/java/src/com/android/intentresolver/inject/SingletonModule.kt
index fbda8be6..36adf06b 100644
--- a/java/src/com/android/intentresolver/inject/SingletonModule.kt
+++ b/java/src/com/android/intentresolver/inject/SingletonModule.kt
@@ -1,15 +1,22 @@
package com.android.intentresolver.inject
+import android.content.Context
import com.android.intentresolver.logging.EventLogImpl
import dagger.Module
import dagger.Provides
+import dagger.Reusable
import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@InstallIn(SingletonComponent::class)
@Module
class SingletonModule {
-
@Provides @Singleton fun instanceIdSequence() = EventLogImpl.newIdSequence()
+
+ @Provides
+ @Reusable
+ @ApplicationOwned
+ fun resources(@ApplicationContext context: Context) = context.resources
}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
new file mode 100644
index 00000000..2da194ca
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
@@ -0,0 +1,392 @@
+/*
+ * 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;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.intentresolver.ChooserRequestParameters;
+import com.android.intentresolver.R;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+ /**
+ * Delegate interface to launch activities when the actions are selected.
+ */
+ public interface ActionActivityStarter {
+ /**
+ * Request an activity launch for the provided target. Implementations may choose to exit
+ * the current activity when the target is launched.
+ */
+ void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+ /**
+ * Request an activity launch for the provided target, optionally employing the specified
+ * shared element transition. Implementations may choose to exit the current activity when
+ * the target is launched.
+ */
+ default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo info, View sharedElement, String sharedElementName) {
+ safelyStartActivityAsPersonalProfileUser(info);
+ }
+ }
+
+ private static final String TAG = "ChooserActions";
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ // Boolean extra used to inform the editor that it may want to customize the editing experience
+ // for the sharesheet editing flow.
+ private static final String EDIT_SOURCE = "edit_source";
+ private static final String EDIT_SOURCE_SHARESHEET = "sharesheet";
+
+ private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
+ private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
+
+ private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private final Context mContext;
+
+ @Nullable
+ private final Runnable mCopyButtonRunnable;
+ private final Runnable mEditButtonRunnable;
+ private final ImmutableList<ChooserAction> mCustomActions;
+ private final @Nullable ChooserAction mModifyShareAction;
+ private final Consumer<Boolean> mExcludeSharedTextAction;
+ private final Consumer</* @Nullable */ Integer> mFinishCallback;
+ private final EventLog mLog;
+
+ /**
+ * @param context
+ * @param chooserRequest data about the invocation of the current Sharesheet session.
+ * @param imageEditor an explicit Activity to launch for editing images
+ * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+ * setting is updated. The argument is whether the shared text is to be excluded.
+ * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+ * View in the Sharesheet UI, if any, or null.
+ * @param activityStarter a delegate to launch activities when actions are selected.
+ * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+ * completed).
+ */
+ public ChooserActionFactory(
+ Context context,
+ ChooserRequestParameters chooserRequest,
+ Optional<ComponentName> imageEditor,
+ EventLog log,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ this(
+ context,
+ makeCopyButtonRunnable(
+ context,
+ chooserRequest.getTargetIntent(),
+ chooserRequest.getReferrerPackageName(),
+ finishCallback,
+ log),
+ makeEditButtonRunnable(
+ getEditSharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ imageEditor),
+ firstVisibleImageQuery,
+ activityStarter,
+ log),
+ chooserRequest.getChooserActions(),
+ chooserRequest.getModifyShareAction(),
+ onUpdateSharedTextIsExcluded,
+ log,
+ finishCallback);
+ }
+
+ @VisibleForTesting
+ ChooserActionFactory(
+ Context context,
+ @Nullable Runnable copyButtonRunnable,
+ Runnable editButtonRunnable,
+ List<ChooserAction> customActions,
+ @Nullable ChooserAction modifyShareAction,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ EventLog log,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ mContext = context;
+ mCopyButtonRunnable = copyButtonRunnable;
+ mEditButtonRunnable = editButtonRunnable;
+ mCustomActions = ImmutableList.copyOf(customActions);
+ mModifyShareAction = modifyShareAction;
+ mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+ mLog = log;
+ mFinishCallback = finishCallback;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getEditButtonRunnable() {
+ return mEditButtonRunnable;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getCopyButtonRunnable() {
+ return mCopyButtonRunnable;
+ }
+
+ /** Create custom actions */
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ List<ActionRow.Action> actions = new ArrayList<>();
+ for (int i = 0; i < mCustomActions.size(); i++) {
+ final int position = i;
+ ActionRow.Action actionRow = createCustomAction(
+ mContext,
+ mCustomActions.get(i),
+ mFinishCallback,
+ () -> {
+ mLog.logCustomActionSelected(position);
+ }
+ );
+ if (actionRow != null) {
+ actions.add(actionRow);
+ }
+ }
+ return actions;
+ }
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Override
+ @Nullable
+ public ActionRow.Action getModifyShareAction() {
+ return createCustomAction(
+ mContext,
+ mModifyShareAction,
+ mFinishCallback,
+ () -> {
+ mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
+ });
+ }
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return mExcludeSharedTextAction;
+ }
+
+ @Nullable
+ private static Runnable makeCopyButtonRunnable(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ Consumer<Integer> finishCallback,
+ EventLog log) {
+ final ClipData clipData;
+ try {
+ clipData = extractTextToCopy(targetIntent);
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to extract data to copy", t);
+ return null;
+ }
+ if (clipData == null) {
+ return null;
+ }
+ return () -> {
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+ log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ @Nullable
+ private static ClipData extractTextToCopy(Intent targetIntent) {
+ if (targetIntent == null) {
+ return null;
+ }
+
+ final String action = targetIntent.getAction();
+
+ ClipData clipData = null;
+ if (Intent.ACTION_SEND.equals(action)) {
+ String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
+
+ if (extraText != null) {
+ clipData = ClipData.newPlainText(null, extraText);
+ } else {
+ Log.w(TAG, "No data available to copy to clipboard");
+ }
+ } else {
+ // expected to only be visible with ACTION_SEND (when a text is shared)
+ Log.d(TAG, "Action (" + action + ") not supported for copying to clipboard");
+ }
+ return clipData;
+ }
+
+ private static TargetInfo getEditSharingTarget(
+ Context context,
+ Intent originalIntent,
+ Optional<ComponentName> imageEditor) {
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ // Retain only URI permission grant flags if present. Other flags may prevent the scene
+ // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+ // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+ resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
+ imageEditor.ifPresent(resolveIntent::setComponent);
+ resolveIntent.setAction(Intent.ACTION_EDIT);
+ resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET);
+ String originalAction = originalIntent.getAction();
+ if (Intent.ACTION_SEND.equals(originalAction)) {
+ if (resolveIntent.getData() == null) {
+ Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ String mimeType = context.getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified editor (" + imageEditor + ") not available");
+ return null;
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ context.getString(R.string.screenshot_edit),
+ "",
+ resolveIntent);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ return dri;
+ }
+
+ private static Runnable makeEditButtonRunnable(
+ TargetInfo editSharingTarget,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ EventLog log) {
+ return () -> {
+ // Log share completion via edit.
+ log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
+
+ View firstImageView = null;
+ try {
+ firstImageView = firstVisibleImageQuery.call();
+ } catch (Exception e) { /* ignore */ }
+ // Action bar is user-independent; always start as primary.
+ if (firstImageView == null) {
+ activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+ } else {
+ activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+ }
+ };
+ }
+
+ @Nullable
+ private static ActionRow.Action createCustomAction(
+ Context context,
+ ChooserAction action,
+ Consumer<Integer> finishCallback,
+ Runnable loggingRunnable) {
+ if (action == null || action.getAction() == null) {
+ return null;
+ }
+ Drawable icon = action.getIcon().loadDrawable(context);
+ if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+ return null;
+ }
+ return new ActionRow.Action(
+ action.getLabel(),
+ icon,
+ () -> {
+ try {
+ action.getAction().send(
+ null,
+ 0,
+ null,
+ null,
+ null,
+ null,
+ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+ }
+ if (loggingRunnable != null) {
+ loggingRunnable.run();
+ }
+ finishCallback.accept(Activity.RESULT_OK);
+ }
+ );
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
index 9a8b0e2a..12e708f6 100644
--- a/java/src/com/android/intentresolver/v2/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -26,8 +26,6 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S
import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
-import static com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL;
-import static com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK;
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import android.annotation.IntDef;
@@ -76,9 +74,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
-import com.android.intentresolver.ChooserActionFactory;
import com.android.intentresolver.ChooserGridLayoutManager;
-import com.android.intentresolver.ChooserIntegratedDeviceComponents;
import com.android.intentresolver.ChooserListAdapter;
import com.android.intentresolver.ChooserRefinementManager;
import com.android.intentresolver.ChooserRequestParameters;
@@ -91,7 +87,6 @@ import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter;
import com.android.intentresolver.ResolverListController;
import com.android.intentresolver.ResolverViewPager;
-import com.android.intentresolver.SecureSettings;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -113,12 +108,15 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.intentresolver.v2.Hilt_ChooserActivity;
+import com.android.intentresolver.v2.platform.ImageEditor;
+import com.android.intentresolver.v2.platform.NearbyShare;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import dagger.hilt.android.AndroidEntryPoint;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
@@ -129,14 +127,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import javax.inject.Inject;
-import dagger.hilt.android.AndroidEntryPoint;
-
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
@@ -195,8 +192,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Inject public FeatureFlags mFeatureFlags;
@Inject public EventLog mEventLog;
-
- private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+ @Inject @ImageEditor public Optional<ComponentName> mImageEditor;
+ @Inject @NearbyShare public Optional<ComponentName> mNearbyShare;
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
* only assignment there, and expect it to be ready by the time we ever use it --
@@ -296,8 +293,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
getEventLog().logSharesheetTriggered();
- mIntegratedDeviceComponents = getIntegratedDeviceComponents();
-
mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
mRefinementManager.getRefinementCompletion().observe(this, completion -> {
@@ -373,11 +368,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mEnterTransitionAnimationDelegate.postponeTransition();
}
- @VisibleForTesting
- protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
- return ChooserIntegratedDeviceComponents.get(this, new SecureSettings());
- }
-
@Override
protected int appliedThemeResId() {
return R.style.Theme_DeviceDefault_Chooser;
@@ -1291,7 +1281,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (appPredictor != null) {
resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
getReferrerPackageName(), appPredictor, userHandle, getEventLog(),
- getIntegratedDeviceComponents().getNearbySharingComponent());
+ mNearbyShare.orElse(null));
} else {
resolverComparator =
new ResolverRankerServiceResolverComparator(
@@ -1301,7 +1291,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
null,
getEventLog(),
getResolverRankerServiceUserHandleList(userHandle),
- getIntegratedDeviceComponents().getNearbySharingComponent());
+ mNearbyShare.orElse(null));
}
return new ChooserListController(
@@ -1323,7 +1313,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return new ChooserActionFactory(
this,
mChooserRequest,
- mIntegratedDeviceComponents,
+ mImageEditor,
getEventLog(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
diff --git a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
new file mode 100644
index 00000000..efbf053e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
@@ -0,0 +1,35 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import androidx.annotation.StringRes
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+internal fun Resources.componentName(@StringRes resId: Int): ComponentName? {
+ check(getResourceTypeName(resId) == "string") { "resId must be a string" }
+ return ComponentName.unflattenFromString(getString(resId))
+}
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ImageEditorModule {
+ /**
+ * The name of the preferred Activity to launch for editing images. This is added to Intents to
+ * edit images using Intent.ACTION_EDIT.
+ */
+ @Provides
+ @Singleton
+ @ImageEditor
+ fun imageEditorComponent(@ApplicationOwned resources: Resources) =
+ Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor))
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
new file mode 100644
index 00000000..25ee9198
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
@@ -0,0 +1,32 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NearbyShareModule {
+
+ @Provides
+ @Singleton
+ @NearbyShare
+ fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) =
+ Optional.ofNullable(
+ ComponentName.unflattenFromString(
+ settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null }
+ ?: resources.getString(R.string.config_defaultNearbySharingComponent),
+ )
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
new file mode 100644
index 00000000..531152ba
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
@@ -0,0 +1,30 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ContentResolver
+import android.provider.Settings
+import javax.inject.Inject
+
+/**
+ * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver.
+ *
+ * These methods make Binder calls and may block, so use on the Main thread should be avoided.
+ */
+class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) :
+ SecureSettings {
+
+ override fun getString(name: String): String? {
+ return Settings.Secure.getString(resolver, name)
+ }
+
+ override fun getInt(name: String): Int? {
+ return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull()
+ }
+
+ override fun getLong(name: String): Long? {
+ return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull()
+ }
+
+ override fun getFloat(name: String): Float? {
+ return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull()
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
new file mode 100644
index 00000000..62ee8ae9
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
@@ -0,0 +1,25 @@
+package com.android.intentresolver.v2.platform
+
+import android.provider.Settings.SettingNotFoundException
+
+/**
+ * A component which provides access to values from [android.provider.Settings.Secure].
+ *
+ * All methods return nullable types instead of throwing [SettingNotFoundException] which yields
+ * cleaner, more idiomatic Kotlin code:
+ *
+ * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO
+ *
+ * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value
+ * missing")
+ */
+interface SecureSettings {
+
+ fun getString(name: String): String?
+
+ fun getInt(name: String): Int?
+
+ fun getLong(name: String): Long?
+
+ fun getFloat(name: String): Float?
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
new file mode 100644
index 00000000..18f47023
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
@@ -0,0 +1,14 @@
+package com.android.intentresolver.v2.platform
+
+import dagger.Binds
+import dagger.Module
+import dagger.Reusable
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface SecureSettingsModule {
+
+ @Binds @Reusable fun secureSettings(settings: PlatformSecureSettings): SecureSettings
+}