summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author mrenouf <mrenouf@google.com> 2023-12-01 13:48:32 -0500
committer Mark Renouf <mrenouf@google.com> 2024-01-18 21:10:00 -0500
commit5e5dd511a3031df38dfe35ca741e31ca9f0eec65 (patch)
treed07a6d2a468abc339630081ffb6a654ca4dde8a8 /java/src
parent99c9828d732ff25c87e8b41e386131dae70b4652 (diff)
Refactor ChooserRequestParameters usage
Creates ChooserRequest data class Uses validation lib to implement parsing of source data Introduces ChooserViewModel as a new target to begin migration of control flow, data and dependencies out of ChooserActivity and into smaller testable units. Test: atest IntentResolver-tests-activity:com.android.intentresolver.v2 Bug: 309960444 Change-Id: I39b3517ec9e17525441d349b3da139ad5956c600
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt5
-rw-r--r--java/src/com/android/intentresolver/v2/ActivityLogic.kt17
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java106
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivity.java17
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt4
-rw-r--r--java/src/com/android/intentresolver/v2/ext/IntentExt.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt59
-rw-r--r--java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt180
-rw-r--r--java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt157
-rw-r--r--java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt55
-rw-r--r--java/src/com/android/intentresolver/v2/validation/ValidationResult.kt2
13 files changed, 577 insertions, 104 deletions
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
index 10ee5af1..4c781a46 100644
--- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -19,14 +19,10 @@ package com.android.intentresolver.contentpreview
import android.content.Intent
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
-import com.android.intentresolver.ChooserRequestParameters
/** A contract for the preview view model. Added for testing. */
abstract class BasePreviewViewModel : ViewModel() {
- @MainThread
- abstract fun createOrReuseProvider(
- targetIntent: Intent
- ): PreviewDataProvider
+ @MainThread abstract fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider
@MainThread abstract fun createOrReuseImageLoader(): ImageLoader
}
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 6350756e..9acc4689 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -24,7 +24,6 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
-import com.android.intentresolver.ChooserRequestParameters
import com.android.intentresolver.R
import com.android.intentresolver.inject.Background
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -45,9 +44,7 @@ constructor(
private var imageLoader: ImagePreviewImageLoader? = null
@MainThread
- override fun createOrReuseProvider(
- targetIntent: Intent
- ): PreviewDataProvider =
+ override fun createOrReuseProvider(targetIntent: Intent): PreviewDataProvider =
previewDataProvider
?: PreviewDataProvider(
viewModelScope + dispatcher,
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
index 7062da33..b9686418 100644
--- a/java/src/com/android/intentresolver/v2/ActivityLogic.kt
+++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2024 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.content.Intent
@@ -18,8 +33,6 @@ import com.android.intentresolver.WorkProfileAvailabilityManager
interface ActivityLogic : CommonActivityLogic {
/** The intent for the target. This will always come before additional targets, if any. */
val targetIntent: Intent
- /** Whether the intent is for home. */
- val resolvingHome: Boolean
/** Custom title to display. */
val title: CharSequence?
/** Resource ID for the title to display when there is no custom title. */
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
index e093058a..a71de19d 100644
--- a/java/src/com/android/intentresolver/v2/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -95,7 +95,9 @@ import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
+import androidx.lifecycle.SavedStateHandleSupport;
import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
@@ -104,7 +106,6 @@ import com.android.intentresolver.AnnotatedUserHandles;
import com.android.intentresolver.ChooserGridLayoutManager;
import com.android.intentresolver.ChooserListAdapter;
import com.android.intentresolver.ChooserRefinementManager;
-import com.android.intentresolver.ChooserRequestParameters;
import com.android.intentresolver.ChooserStackedAppDialogFragment;
import com.android.intentresolver.ChooserTargetActionsDialogFragment;
import com.android.intentresolver.EnterTransitionAnimationDelegate;
@@ -147,6 +148,9 @@ import com.android.intentresolver.v2.platform.AppPredictionAvailable;
import com.android.intentresolver.v2.platform.ImageEditor;
import com.android.intentresolver.v2.platform.NearbyShare;
import com.android.intentresolver.v2.ui.ActionTitle;
+import com.android.intentresolver.v2.ui.model.CallerInfo;
+import com.android.intentresolver.v2.ui.model.ChooserRequest;
+import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
@@ -311,31 +315,46 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
private boolean mFinishWhenStopped = false;
private final AtomicLong mIntentReceivedTime = new AtomicLong(-1);
+ private ChooserViewModel mViewModel;
@VisibleForTesting
- protected ActivityLogic createActivityLogic() {
+ protected ChooserActivityLogic createActivityLogic(ChooserRequest chooserRequest) {
return new ChooserActivityLogic(
TAG,
/* activity = */ this,
- this::onWorkProfileStatusUpdated);
+ this::onWorkProfileStatusUpdated,
+ chooserRequest);
+ }
+
+ @NonNull
+ @Override
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ CreationExtras extras = super.getDefaultViewModelCreationExtras();
+ // Inserts a CallerInfo into the Bundle at stored at DEFAULT_ARGS_KEY
+ Bundle defaultArgs = requireNonNull(extras.get(SavedStateHandleSupport.DEFAULT_ARGS_KEY));
+ defaultArgs.putParcelable(CallerInfo.SAVED_STATE_HANDLE_KEY,
+ new CallerInfo(getLaunchedFromUid(),
+ getLaunchedFromPackage(),
+ requireNonNull(getReferrer())));
+ return extras;
}
@Override
protected final void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
- if (isFinishing()) {
- // Performing a clean exit:
- // Skip initializing any additional resources.
- return;
- }
setTheme(R.style.Theme_DeviceDefault_Chooser);
- mLogic = createActivityLogic();
Tracer.INSTANCE.markLaunched();
+ mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class);
+ if (!mViewModel.init()) {
+ finish();
+ return;
+ }
+ mLogic = createActivityLogic(mViewModel.getChooserRequest());
+ init();
}
- @Override
- protected final void onPostCreate(@Nullable Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
+ private void init() {
mIntentReceivedTime.set(System.currentTimeMillis());
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
@@ -345,21 +364,16 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mShouldDisplayLandscape =
shouldDisplayLandscape(getResources().getConfiguration().orientation);
- ChooserRequestParameters chooserRequest = getChooserRequest();
- if (chooserRequest == null) {
- finish();
- return;
- }
-
+ ChooserRequest chooserRequest = mViewModel.getChooserRequest();
setRetainInOnStop(chooserRequest.shouldRetainInOnStop());
createProfileRecords(
new AppPredictorFactory(
this,
- chooserRequest.getSharedText(),
- chooserRequest.getTargetIntentFilter(),
+ Objects.toString(chooserRequest.getSharedText(), null),
+ chooserRequest.getShareTargetFilter(),
mAppPredictionAvailable
),
- chooserRequest.getTargetIntentFilter()
+ chooserRequest.getShareTargetFilter()
);
Intent intent = mLogic.getTargetIntent();
@@ -493,8 +507,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mLogic.getReferrerPackageName(),
chooserRequest.getTargetType(),
chooserRequest.getCallerChooserTargets().size(),
- (chooserRequest.getInitialIntents() == null)
- ? 0 : chooserRequest.getInitialIntents().length,
+ chooserRequest.getInitialIntents().size(),
isWorkProfile(),
mChooserContentPreviewUi.getPreferredContentPreview(),
chooserRequest.getTargetAction(),
@@ -502,8 +515,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
chooserRequest.getModifyShareAction() != null
);
mEnterTransitionAnimationDelegate.postponeTransition();
-
- restore(savedInstanceState);
}
private void restore(@Nullable Bundle savedInstanceState) {
@@ -1151,15 +1162,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
- @Nullable
- private ChooserRequestParameters getChooserRequest() {
- return ((ChooserActivityLogic) mLogic).getChooserRequestParameters();
- }
-
- private ChooserRequestParameters requireChooserRequest() {
- return requireNonNull(getChooserRequest());
- }
-
private AnnotatedUserHandles requireAnnotatedUserHandles() {
return requireNonNull(mLogic.getAnnotatedUserHandles());
}
@@ -1234,7 +1236,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
protected EmptyStateProvider createBlockerEmptyStateProvider() {
- final boolean isSendAction = requireChooserRequest().isSendActionTarget();
+ final boolean isSendAction = mViewModel.getChooserRequest().isSendActionTarget();
final EmptyState noWorkToPersonalEmptyState =
new DevicePolicyBlockerEmptyState(
@@ -1504,7 +1506,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
final Intent intent = getIntent();
if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
- && !mLogic.getResolvingHome() && !mRetainInOnStop) {
+ && !mRetainInOnStop) {
// This resolver is in the unusual situation where it has been
// launched at the top of a new task. We don't let it be added
// to the recent tasks shown to the user, and we need to make sure
@@ -1550,10 +1552,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
- ChooserRequestParameters chooserRequest = getChooserRequest();
- if (chooserRequest == null) {
- return defIntent;
- }
+ ChooserRequest chooserRequest = mViewModel.getChooserRequest();
Intent result = defIntent;
if (chooserRequest.getReplacementExtras() != null) {
@@ -1578,7 +1577,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
public void onActivityStarted(TargetInfo cti) {
- ChooserRequestParameters chooserRequest = requireChooserRequest();
+ ChooserRequest chooserRequest = mViewModel.getChooserRequest();
if (chooserRequest.getChosenComponentSender() != null) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
@@ -1595,7 +1594,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private void addCallerChooserTargets() {
- ChooserRequestParameters chooserRequest = requireChooserRequest();
+ ChooserRequest chooserRequest = mViewModel.getChooserRequest();
if (!chooserRequest.getCallerChooserTargets().isEmpty()) {
// Send the caller's chooser targets only to the default profile.
if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) {
@@ -1637,8 +1636,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
// TODO: implement these type-conditioned behaviors polymorphically, and consider moving
// the logic into `ChooserTargetActionsDialogFragment.show()`.
boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
- IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
- ? requireChooserRequest().getTargetIntentFilter() : null;
+ IntentFilter intentFilter;
+ intentFilter = targetInfo.isSelectableTargetInfo()
+ ? mViewModel.getChooserRequest().getShareTargetFilter() : null;
String shortcutTitle = targetInfo.isSelectableTargetInfo()
? targetInfo.getDisplayLabel().toString() : null;
String shortcutIdKey = targetInfo.getDirectShareShortcutId();
@@ -1658,7 +1658,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
protected boolean onTargetSelected(TargetInfo target) {
if (mRefinementManager.maybeHandleSelection(
target,
- requireChooserRequest().getRefinementIntentSender(),
+ mViewModel.getChooserRequest().getRefinementIntentSender(),
getApplication(),
getMainThreadHandler())) {
return false;
@@ -1732,7 +1732,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
targetInfo.getResolveInfo().activityInfo.processName,
which,
/* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
- requireChooserRequest().getCallerChooserTargets().size(),
+ mViewModel.getChooserRequest().getCallerChooserTargets().size(),
targetInfo.getHashedTargetIdForMetrics(this),
targetInfo.isPinned(),
mIsSuccessfullySelected,
@@ -1839,7 +1839,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (targetIntent == null) {
return;
}
- Intent originalTargetIntent = new Intent(requireChooserRequest().getTargetIntent());
+ Intent originalTargetIntent = new Intent(mViewModel.getChooserRequest().getTargetIntent());
// Our TargetInfo implementations add associated component to the intent, let's do the same
// for the sake of the comparison below.
if (targetIntent.getComponent() != null) {
@@ -1938,7 +1938,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Override
public boolean isComponentFiltered(ComponentName name) {
- return requireChooserRequest().getFilteredComponentNames().contains(name);
+ return mViewModel.getChooserRequest().getFilteredComponentNames().contains(name);
}
@Override
@@ -1955,7 +1955,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
List<ResolveInfo> rList,
boolean filterLastUsed,
UserHandle userHandle) {
- ChooserRequestParameters parameters = requireChooserRequest();
+ ChooserRequest parameters = mViewModel.getChooserRequest();
ChooserListAdapter chooserListAdapter = createChooserListAdapter(
context,
payloadIntents,
@@ -2104,11 +2104,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private ChooserActionFactory createChooserActionFactory() {
- ChooserRequestParameters request = requireChooserRequest();
+ ChooserRequest request = mViewModel.getChooserRequest();
return new ChooserActionFactory(
this,
request.getTargetIntent(),
- request.getReferrerPackageName(),
+ request.getLaunchedFromPackage(),
request.getChooserActions(),
request.getModifyShareAction(),
mImageEditor,
@@ -2473,7 +2473,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
- ChooserRequestParameters chooserRequest = getChooserRequest();
+ ChooserRequest chooserRequest = mViewModel.getChooserRequest();
return (chooserRequest != null) && chooserRequest.isSendActionTarget();
}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
index a8150f52..f6054885 100644
--- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
+++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
@@ -1,11 +1,9 @@
package com.android.intentresolver.v2
-import android.app.Activity
import android.content.Intent
-import android.util.Log
import androidx.activity.ComponentActivity
import androidx.annotation.OpenForTesting
-import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.v2.ui.model.ChooserRequest
private const val TAG = "ChooserActivityLogic"
@@ -13,14 +11,14 @@ private const val TAG = "ChooserActivityLogic"
* Activity logic for [ChooserActivity].
*
* TODO: Make this class no longer open once [ChooserActivity] no longer needs to cast to access
- * [chooserRequestParameters]. For now, this class being open is better than using reflection
- * there.
+ * [chooserRequest]. For now, this class being open is better than using reflection there.
*/
@OpenForTesting
open class ChooserActivityLogic(
tag: String,
activity: ComponentActivity,
- onWorkProfileStatusUpdated: () -> Unit
+ onWorkProfileStatusUpdated: () -> Unit,
+ private val chooserRequest: ChooserRequest? = null,
) :
ActivityLogic,
CommonActivityLogic by CommonActivityLogicImpl(
@@ -29,30 +27,16 @@ open class ChooserActivityLogic(
onWorkProfileStatusUpdated,
) {
- val chooserRequestParameters: ChooserRequestParameters? =
- try {
- ChooserRequestParameters(
- (activity as Activity).intent,
- referrerPackageName,
- (activity as Activity).referrer,
- )
- } catch (e: IllegalArgumentException) {
- Log.e(tag, "Caller provided invalid Chooser request parameters", e)
- null
- }
+ override val targetIntent: Intent = chooserRequest?.targetIntent ?: Intent()
- override val targetIntent: Intent = chooserRequestParameters?.targetIntent ?: Intent()
+ override val title: CharSequence? = chooserRequest?.title
- override val resolvingHome: Boolean = false
+ override val defaultTitleResId: Int = chooserRequest?.defaultTitleResource ?: 0
- override val title: CharSequence? = chooserRequestParameters?.title
-
- override val defaultTitleResId: Int = chooserRequestParameters?.defaultTitleResource ?: 0
-
- override val initialIntents: List<Intent>? = chooserRequestParameters?.initialIntents?.toList()
+ override val initialIntents: List<Intent>? = chooserRequest?.initialIntents?.toList()
override val payloadIntents: List<Intent> = buildList {
add(targetIntent)
- chooserRequestParameters?.additionalTargets?.let { addAll(it) }
+ chooserRequest?.additionalTargets?.let { addAll(it) }
}
}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java
index 9672e9d6..0e526b4c 100644
--- a/java/src/com/android/intentresolver/v2/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java
@@ -106,6 +106,7 @@ import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvide
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.ext.IntentExtKt;
import com.android.intentresolver.v2.ui.ActionTitle;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
@@ -141,6 +142,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
protected ActivityLogic mLogic;
protected TargetDataLoader mTargetDataLoader;
+ private boolean mResolvingHome;
private Button mAlwaysButton;
private Button mOnceButton;
@@ -223,7 +225,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
}
@VisibleForTesting
- protected ActivityLogic createActivityLogic() {
+ protected ResolverActivityLogic createActivityLogic() {
return new ResolverActivityLogic(
TAG,
/* activity = */ this,
@@ -235,6 +237,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
super.onCreate(savedInstanceState);
setTheme(R.style.Theme_DeviceDefault_Resolver);
mLogic = createActivityLogic();
+ mResolvingHome = IntentExtKt.isHomeIntent(getIntent());
mTargetDataLoader = new DefaultTargetDataLoader(
this,
getLifecycle(),
@@ -242,11 +245,6 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE,
/* defaultValue = */ false)
);
- }
-
- @Override
- protected final void onPostCreate(@Nullable Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
init();
restore(savedInstanceState);
}
@@ -486,7 +484,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
}
final Intent intent = getIntent();
if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
- && !mLogic.getResolvingHome()) {
+ && !mResolvingHome) {
// This resolver is in the unusual situation where it has been
// launched at the top of a new task. We don't let it be added
// to the recent tasks shown to the user, and we need to make sure
@@ -532,7 +530,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
}
ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
.resolveInfoForPosition(which, hasIndexBeenFiltered);
- if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString();
Toast.makeText(this,
mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName),
@@ -1133,7 +1131,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
}
protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
- final ActionTitle title = mLogic.getResolvingHome()
+ final ActionTitle title = mResolvingHome
? ActionTitle.HOME
: ActionTitle.forAction(intent.getAction());
@@ -1198,7 +1196,6 @@ public class ResolverActivity extends Hilt_ResolverActivity implements
@Override
protected final void onStart() {
super.onStart();
-
this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
if (hasWorkProfile()) {
mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this);
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
index cf843043..13353041 100644
--- a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
+++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
@@ -36,10 +36,6 @@ open class ResolverActivityLogic(
intent
}
- override val resolvingHome: Boolean =
- targetIntent.action == Intent.ACTION_MAIN &&
- targetIntent.categories.singleOrNull() == Intent.CATEGORY_HOME
-
override val title: CharSequence? = null
override val defaultTitleResId: Int = 0
diff --git a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt
new file mode 100644
index 00000000..7aa8e036
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 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.ext
+
+import android.content.Intent
+import java.util.function.Predicate
+
+/** Applies an operation on this Intent if matches the given filter. */
+inline fun Intent.ifMatch(
+ predicate: Predicate<Intent>,
+ crossinline block: Intent.() -> Unit
+): Intent {
+ if (predicate.test(this)) {
+ apply(block)
+ }
+ return this
+}
+
+/** True if the Intent has one of the specified actions. */
+fun Intent.hasAction(vararg actions: String): Boolean = action in actions
+
+/** True if the Intent has a single matching category. */
+fun Intent.hasSingleCategory(category: String) = categories.singleOrNull() == category
+
+/** True if the Intent resolves to the special Home (Launcher) component */
+fun Intent.isHomeIntent() = hasAction(Intent.ACTION_MAIN) && hasSingleCategory(Intent.CATEGORY_HOME)
diff --git a/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt b/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt
new file mode 100644
index 00000000..9addeef2
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 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.model
+
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+
+data class CallerInfo(
+ val launchedFromUid: Int,
+ val launchedFomPackage: String?,
+ /* logged to metrics, forwarded to outgoing intent */
+ val referrer: Uri
+) : Parcelable {
+ constructor(
+ source: Parcel
+ ) : this(
+ launchedFromUid = source.readInt(),
+ launchedFomPackage = source.readString(),
+ checkNotNull(source.readParcelable())
+ )
+
+ override fun describeContents() = 0 /* flags */
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeInt(launchedFromUid)
+ dest.writeString(launchedFomPackage)
+ dest.writeParcelable(referrer, 0)
+ }
+
+ companion object {
+ const val SAVED_STATE_HANDLE_KEY = "com.android.intentresolver.CALLER_INFO"
+
+ @JvmStatic
+ @Suppress("unused")
+ val CREATOR =
+ object : Parcelable.Creator<CallerInfo> {
+ override fun newArray(size: Int) = arrayOfNulls<CallerInfo>(size)
+ override fun createFromParcel(source: Parcel) = CallerInfo(source)
+ }
+ }
+}
+
+inline fun <reified T> Parcel.readParcelable(): T? {
+ return readParcelable(T::class.java.classLoader, T::class.java)
+}
diff --git a/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt
new file mode 100644
index 00000000..2fbf94a2
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/model/ChooserRequest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2024 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.model
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.os.Bundle
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import androidx.annotation.StringRes
+import com.android.intentresolver.v2.ext.hasAction
+
+const val MAX_CHOOSER_ACTIONS = 5
+const val MAX_INITIAL_INTENTS = 2
+
+/** All of the things that are consumed from an incoming share Intent (+Extras). */
+data class ChooserRequest(
+ /** Required. Represents the content being sent. */
+ val targetIntent: Intent,
+
+ /** The action from [targetIntent] as retrieved with [Intent.getAction]. */
+ val targetAction: String?,
+
+ /**
+ * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the
+ * canonical "Share" actions. When handling other actions, this flag controls behavioral and
+ * visual changes.
+ */
+ val isSendActionTarget: Boolean,
+
+ /** The top-level content type as retrieved using [Intent.getType]. */
+ val targetType: String?,
+
+ /** The package name of the app which started the current activity instance. */
+ val launchedFromPackage: String,
+
+ /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */
+ val title: CharSequence? = null,
+
+ /** A String resource ID to load when [title] is null. */
+ @get:StringRes val defaultTitleResource: Int = 0,
+
+ /**
+ * An empty intent which carries an extra of [Intent.EXTRA_REFERRER]. To be merged with outgoing
+ * intents. This provides the original referrer value to the target.
+ */
+ val referrerFillInIntent: Intent,
+
+ /**
+ * Choices to exclude from results.
+ *
+ * Any resolved intents with a component in this list will be omitted before presentation.
+ */
+ val filteredComponentNames: List<ComponentName> = emptyList(),
+
+ /**
+ * App provided shortcut share intents (aka "direct share targets")
+ *
+ * Normally share shortcuts are published and consumed using
+ * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow
+ * apps to directly inject the same information.
+ *
+ * Historical note: This option was initially integrated with other results from the
+ * ChooserTargetService API (since deprecated and removed), hence the name and data format.
+ * These are more correctly called "Share Shortcuts" now.
+ */
+ val callerChooserTargets: List<ChooserTarget> = emptyList(),
+
+ /**
+ * Actions the user may perform. These are presented as separate affordances from the main list
+ * of choices. Selecting a choice is a terminal action which results in finishing. The item
+ * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate.
+ */
+ val chooserActions: List<ChooserAction> = emptyList(),
+
+ /**
+ * An action to start an Activity which for user updating of shared content. Selection is a
+ * terminal action, closing the current activity and launching the target of the action.
+ */
+ val modifyShareAction: ChooserAction? = null,
+
+ /**
+ * When false the host activity will be [finished][android.app.Activity.finish] when stopped.
+ */
+ @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false,
+
+ /**
+ * Intents which contain alternate representations of the content being shared. Any results from
+ * resolving these _alternate_ intents are included with the results of the primary intent as
+ * additional choices (e.g. share as image content vs. link to content).
+ */
+ val additionalTargets: List<Intent> = emptyList(),
+
+ /**
+ * Alternate [extras][Intent.getExtras] to substitute when launching a selected app.
+ *
+ * For a given app (by package name), the Bundle describes what parameters to substitute when
+ * that app is selected.
+ *
+ * // TODO: Map<String, Bundle>
+ */
+ val replacementExtras: Bundle? = null,
+
+ /**
+ * App-supplied choices to be presented first in the list.
+ *
+ * Custom labels and icons may be supplied using
+ * [LabeledIntent][android.content.pm.LabeledIntent].
+ *
+ * Limit 2.
+ */
+ val initialIntents: List<Intent> = emptyList(),
+
+ /**
+ * Provides for callers to be notified when a component is selected.
+ *
+ * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the
+ * [ComponentName] of the item.
+ */
+ val chosenComponentSender: IntentSender? = null,
+
+ /**
+ * Provides a mechanism for callers to post-process a target when a selection is made.
+ *
+ * The received intent will contain:
+ * * **EXTRA_INTENT** The chosen target
+ * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target
+ * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a
+ * mechanism for the caller to return information. An updated intent to send must be included
+ * as [Intent.EXTRA_INTENT].
+ */
+ val refinementIntentSender: IntentSender? = null,
+
+ /**
+ * Contains the text content to share supplied by the source app.
+ *
+ * TODO: Constrain length?
+ */
+ val sharedText: CharSequence? = null,
+
+ /**
+ * Supplied to
+ * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to
+ * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType]
+ * are considered for matching share shortcuts currently.
+ */
+ val shareTargetFilter: IntentFilter? = null
+) {
+
+ /** Constructs an instance from only the required values. */
+ constructor(
+ targetIntent: Intent,
+ referrerPackageName: String
+ ) : this(
+ targetIntent,
+ targetIntent.action,
+ targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE),
+ targetIntent.type,
+ referrerPackageName,
+ referrerFillInIntent =
+ Intent().apply { putExtra(Intent.EXTRA_REFERRER, referrerPackageName) }
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
new file mode 100644
index 00000000..6878be5f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2024 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.viewmodel
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.EXTRA_ALTERNATE_INTENTS
+import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
+import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
+import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_TARGETS
+import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER
+import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS
+import android.content.Intent.EXTRA_INITIAL_INTENTS
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_REFERRER
+import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.EXTRA_TITLE
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.os.Bundle
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.ChooserActivity
+import com.android.intentresolver.R
+import com.android.intentresolver.util.hasValidIcon
+import com.android.intentresolver.v2.ext.hasAction
+import com.android.intentresolver.v2.ext.ifMatch
+import com.android.intentresolver.v2.ui.model.CallerInfo
+import com.android.intentresolver.v2.ui.model.ChooserRequest
+import com.android.intentresolver.v2.ui.model.MAX_CHOOSER_ACTIONS
+import com.android.intentresolver.v2.ui.model.MAX_INITIAL_INTENTS
+import com.android.intentresolver.v2.validation.types.IntentOrUri
+import com.android.intentresolver.v2.validation.types.array
+import com.android.intentresolver.v2.validation.types.value
+import com.android.intentresolver.v2.validation.validateFrom
+
+private fun Intent.hasSendAction() = hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE)
+
+internal fun Intent.maybeAddSendActionFlags() =
+ ifMatch(Intent::hasSendAction) {
+ addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
+ addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
+ }
+
+fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) =
+ validateFrom(source) {
+ val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags()
+
+ val isSendAction = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE)
+
+ val additionalTargets =
+ optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() }
+ ?: emptyList()
+
+ val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS))
+
+ val (customTitle, defaultTitleResource) =
+ if (isSendAction) {
+ ignored(
+ value<CharSequence>(EXTRA_TITLE),
+ "deprecated in P. You may wish to set a preview title by using EXTRA_TITLE " +
+ "property of the wrapped EXTRA_INTENT."
+ )
+ null to R.string.chooseActivity
+ } else {
+ val custom = optional(value<CharSequence>(EXTRA_TITLE))
+ custom to (custom?.let { 0 } ?: R.string.chooseActivity)
+ }
+
+ val initialIntents =
+ optional(array<Intent>(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map {
+ it.maybeAddSendActionFlags()
+ }
+ ?: emptyList()
+
+ val chosenComponentSender =
+ optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER))
+
+ val refinementIntentSender =
+ optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER))
+
+ val filteredComponents =
+ optional(array<ComponentName>(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList()
+
+ @Suppress("DEPRECATION")
+ val callerChooserTargets =
+ optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) ?: emptyList()
+
+ val retainInOnStop =
+ optional(value<Boolean>(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false
+
+ val sharedText = optional(value<CharSequence>(EXTRA_TEXT))
+
+ val chooserActions =
+ optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS))
+ ?.filter { hasValidIcon(it) }
+ ?.take(MAX_CHOOSER_ACTIONS)
+ ?: emptyList()
+
+ val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION))
+
+ val referrerFillIn = Intent().putExtra(EXTRA_REFERRER, callerInfo.referrer)
+
+ ChooserRequest(
+ targetIntent = targetIntent,
+ targetAction = targetIntent.action,
+ isSendActionTarget = isSendAction,
+ targetType = targetIntent.type,
+ launchedFromPackage =
+ requireNotNull(callerInfo.launchedFomPackage) {
+ "launchedFromPackage was null, See Activity.getLaunchedFromPackage()"
+ },
+ title = customTitle,
+ defaultTitleResource = defaultTitleResource,
+ referrerFillInIntent = referrerFillIn,
+ filteredComponentNames = filteredComponents,
+ callerChooserTargets = callerChooserTargets,
+ chooserActions = chooserActions,
+ modifyShareAction = modifyShareAction,
+ shouldRetainInOnStop = retainInOnStop,
+ additionalTargets = additionalTargets,
+ replacementExtras = replacementExtras,
+ initialIntents = initialIntents,
+ chosenComponentSender = chosenComponentSender,
+ refinementIntentSender = refinementIntentSender,
+ sharedText = sharedText,
+ shareTargetFilter = targetIntent.toShareTargetFilter()
+ )
+ }
+
+private fun Intent.toShareTargetFilter(): IntentFilter? {
+ return type?.let {
+ IntentFilter().apply {
+ action?.also { addAction(it) }
+ addDataType(it)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
new file mode 100644
index 00000000..663235ca
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.v2.ui.model.CallerInfo
+import com.android.intentresolver.v2.ui.model.ChooserRequest
+import com.android.intentresolver.v2.validation.ValidationResult
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+
+private const val TAG = "ChooserViewModel"
+
+@HiltViewModel
+class ChooserViewModel
+@Inject
+constructor(
+ private val args: SavedStateHandle,
+) : ViewModel() {
+
+ private val callerInfo: CallerInfo =
+ requireNotNull(args[CallerInfo.SAVED_STATE_HANDLE_KEY]) {
+ "CallerInfo missing in SavedStateHandle! (${CallerInfo.SAVED_STATE_HANDLE_KEY})"
+ }
+
+ /** The result of reading and validating the inputs provided in savedState. */
+ private val status: ValidationResult<ChooserRequest> = readChooserRequest(callerInfo, args::get)
+
+ val chooserRequest: ChooserRequest by lazy { status.getOrThrow() }
+
+ fun init(): Boolean {
+ Log.i(TAG, "viewModel init")
+ if (!status.isSuccess()) {
+ status.reportToLogcat(TAG)
+ return false
+ }
+ Log.i(TAG, "request = $chooserRequest")
+ return true
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
index 092cabe8..856a521e 100644
--- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
+++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
@@ -26,7 +26,7 @@ sealed interface ValidationResult<T> {
fun getOrThrow(): T =
checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") }
- fun <T> reportToLogcat(tag: String) {
+ fun reportToLogcat(tag: String) {
findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) }
}
}