diff options
| author | 2023-09-11 10:53:02 -0400 | |
|---|---|---|
| committer | 2023-10-10 15:51:55 -0400 | |
| commit | 35018884b00755252f5268507065a7ea6cb9404b (patch) | |
| tree | 642993f8396e95e561a58b6b9022a2614ea24122 /java/src/com | |
| parent | 73bad17b4fa5a7c07409ed6d46121614b48adb33 (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')
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 +} |