diff options
14 files changed, 345 insertions, 84 deletions
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 95d9ea18..c1184a80 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE import static androidx.lifecycle.LifecycleKt.getCoroutineScope; +import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs; import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; @@ -75,7 +76,6 @@ import android.stats.devicepolicy.DevicePolicyEnums; import android.text.TextUtils; import android.util.Log; import android.util.Slog; -import android.util.SparseArray; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -95,7 +95,6 @@ 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; @@ -149,7 +148,7 @@ 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.ActivityLaunch; import com.android.intentresolver.v2.ui.model.ChooserRequest; import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel; import com.android.intentresolver.widget.ImagePreviewView; @@ -164,6 +163,7 @@ import com.google.common.collect.ImmutableList; import dagger.hilt.android.AndroidEntryPoint; +import kotlin.Pair; import kotlin.Unit; import java.util.ArrayList; @@ -265,6 +265,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1; private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2; + @Inject public ActivityLaunch mActivityLaunch; @Inject public FeatureFlags mFeatureFlags; @Inject public EventLog mEventLog; @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; @@ -332,20 +333,21 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @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; + return addDefaultArgs( + super.getDefaultViewModelCreationExtras(), + new Pair<>(ActivityLaunch.ACTIVITY_LAUNCH_KEY, mActivityLaunch)); } @Override protected final void onCreate(Bundle savedInstanceState) { - Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); + Log.i(TAG, "onCreate"); + Log.i(TAG, "activityLaunch=" + mActivityLaunch.toString()); + int callerUid = mActivityLaunch.getFromUid(); + if (callerUid < 0 || UserHandle.isIsolated(callerUid)) { + Log.e(TAG, "Can't start a resolver from uid " + callerUid); + finish(); + } setTheme(R.style.Theme_DeviceDefault_Chooser); Tracer.INSTANCE.markLaunched(); mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class); @@ -824,7 +826,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } } catch (RuntimeException e) { Slog.wtf(TAG, - "Unable to launch as uid " + requireAnnotatedUserHandles().userIdOfCallingApp + "Unable to launch as uid " + mActivityLaunch.getFromUid() + " package " + getLaunchedFromPackage() + ", while running in " + ActivityThread.currentProcessName(), e); } @@ -2103,7 +2105,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mPackageManager, mLogic.getTargetIntent(), mLogic.getReferrerPackageName(), - requireAnnotatedUserHandles().userIdOfCallingApp, + mActivityLaunch.getFromUid(), resolverComparator, getQueryIntentsUser(userHandle)); } diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index 55e698a6..a308ea14 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -24,6 +24,7 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import static com.android.intentresolver.v2.ext.CreationExtrasExtKt.addDefaultArgs; import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; import static java.util.Collections.emptyList; @@ -83,6 +84,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.viewmodel.CreationExtras; import androidx.viewpager.widget.ViewPager; import com.android.intentresolver.AnnotatedUserHandles; @@ -109,6 +111,7 @@ import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider 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.v2.ui.model.ActivityLaunch; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; @@ -119,6 +122,7 @@ import com.google.common.collect.ImmutableList; import dagger.hilt.android.AndroidEntryPoint; +import kotlin.Pair; import kotlin.Unit; import java.util.ArrayList; @@ -140,6 +144,7 @@ import javax.inject.Inject; public class ResolverActivity extends Hilt_ResolverActivity implements ResolverListAdapter.ResolverListCommunicator { + @Inject public ActivityLaunch mActivityLaunch; @Inject public DevicePolicyResources mDevicePolicyResources; @Inject public IntentForwarding mIntentForwarding; @@ -235,10 +240,26 @@ public class ResolverActivity extends Hilt_ResolverActivity implements this::onWorkProfileStatusUpdated); } + @NonNull + @Override + public CreationExtras getDefaultViewModelCreationExtras() { + return addDefaultArgs( + super.getDefaultViewModelCreationExtras(), + new Pair<>(ActivityLaunch.ACTIVITY_LAUNCH_KEY, mActivityLaunch)); + } + @Override protected final void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setTheme(R.style.Theme_DeviceDefault_Resolver); + Log.i(TAG, "onCreate"); + Log.i(TAG, "activityLaunch=" + mActivityLaunch.toString()); + int callerUid = mActivityLaunch.getFromUid(); + if (callerUid < 0 || UserHandle.isIsolated(callerUid)) { + Log.e(TAG, "Can't start a resolver from uid " + callerUid); + finish(); + } + mLogic = createActivityLogic(); mResolvingHome = IntentExtKt.isHomeIntent(getIntent()); mTargetDataLoader = new DefaultTargetDataLoader( @@ -256,13 +277,6 @@ public class ResolverActivity extends Hilt_ResolverActivity implements Intent intent = mLogic.getTargetIntent(); List<Intent> initialIntents = mLogic.getInitialIntents(); - // Calling UID did not have valid permissions - if (mLogic.getAnnotatedUserHandles() == null) { - finish(); - return; - } - - // The last argument of createResolverListAdapter is whether to do special handling // of the last used choice to highlight it in the list. We need to always // turn this off when running under voice interaction, since it results in @@ -760,7 +774,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements getPackageManager(), mLogic.getTargetIntent(), mLogic.getReferrerPackageName(), - requireAnnotatedUserHandles().userIdOfCallingApp, + mActivityLaunch.getFromUid(), resolverComparator, getQueryIntentsUser(userHandle)); } @@ -1486,7 +1500,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements } } catch (RuntimeException e) { Slog.wtf(TAG, - "Unable to launch as uid " + requireAnnotatedUserHandles().userIdOfCallingApp + "Unable to launch as uid " + mActivityLaunch.getFromUid() + " package " + getLaunchedFromPackage() + ", while running in " + ActivityThread.currentProcessName(), e); } diff --git a/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt b/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt new file mode 100644 index 00000000..ebd613f1 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt @@ -0,0 +1,31 @@ +/* + * 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.os.Bundle +import android.os.Parcelable +import androidx.lifecycle.DEFAULT_ARGS_KEY +import androidx.lifecycle.viewmodel.CreationExtras + +/** Adds one or more key-value pairs to the default Args bundle in this extras instance. */ +fun CreationExtras.addDefaultArgs(vararg values: Pair<String, Parcelable>): CreationExtras { + val defaultArgs: Bundle = get(DEFAULT_ARGS_KEY) ?: Bundle() + for ((key, value) in values) { + defaultArgs.putParcelable(key, value) + } + return this +} diff --git a/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt b/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt new file mode 100644 index 00000000..b0ec97f4 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt @@ -0,0 +1,27 @@ +/* + * 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.os.Parcel + +inline fun <reified T> Parcel.requireParcelable(): T { + return requireNotNull(readParcelable<T>()) { "A non-value required from this parcel was null!" } +} + +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/CallerInfo.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt index 9addeef2..fd25ea42 100644 --- a/java/src/com/android/intentresolver/v2/ui/model/CallerInfo.kt +++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunch.kt @@ -15,45 +15,51 @@ */ package com.android.intentresolver.v2.ui.model +import android.content.Intent import android.net.Uri import android.os.Parcel import android.os.Parcelable +import com.android.intentresolver.v2.ext.readParcelable +import com.android.intentresolver.v2.ext.requireParcelable -data class CallerInfo( - val launchedFromUid: Int, - val launchedFomPackage: String?, - /* logged to metrics, forwarded to outgoing intent */ - val referrer: Uri +/** Contains Activity-scope information about the state at launch time. */ +data class ActivityLaunch( + /** The [Intent] received by the app */ + val intent: Intent, + /** The identifier for the sending app and user */ + val fromUid: Int, + /** The package of the sending app */ + val fromPackage: String?, + /** The referrer as supplied to the activity. */ + val referrer: Uri? ) : Parcelable { constructor( source: Parcel ) : this( - launchedFromUid = source.readInt(), - launchedFomPackage = source.readString(), - checkNotNull(source.readParcelable()) + intent = source.requireParcelable(), + fromUid = source.readInt(), + fromPackage = source.readString(), + referrer = source.readParcelable() ) override fun describeContents() = 0 /* flags */ override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeInt(launchedFromUid) - dest.writeString(launchedFomPackage) - dest.writeParcelable(referrer, 0) + dest.writeParcelable(intent, flags) + dest.writeInt(fromUid) + dest.writeString(fromPackage) + dest.writeParcelable(referrer, flags) } companion object { - const val SAVED_STATE_HANDLE_KEY = "com.android.intentresolver.CALLER_INFO" + const val ACTIVITY_LAUNCH_KEY = "com.android.intentresolver.ACTIVITY_LAUNCH" - @JvmStatic + @JvmField @Suppress("unused") val CREATOR = - object : Parcelable.Creator<CallerInfo> { - override fun newArray(size: Int) = arrayOfNulls<CallerInfo>(size) - override fun createFromParcel(source: Parcel) = CallerInfo(source) + object : Parcelable.Creator<ActivityLaunch> { + override fun newArray(size: Int) = arrayOfNulls<ActivityLaunch>(size) + override fun createFromParcel(source: Parcel) = ActivityLaunch(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/ActivityLaunchModule.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt new file mode 100644 index 00000000..3311467e --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/model/ActivityLaunchModule.kt @@ -0,0 +1,40 @@ +/* + * 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.app.Activity +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object ActivityLaunchModule { + + @Provides + @ActivityScoped + fun callerInfo(activity: Activity): ActivityLaunch { + return ActivityLaunch( + activity.intent, + activity.launchedFromUid, + activity.launchedFromPackage, + activity.referrer + ) + } +} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt index 6878be5f..33868aaf 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt @@ -44,10 +44,11 @@ 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.ActivityLaunch 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.ValidationResult import com.android.intentresolver.v2.validation.types.IntentOrUri import com.android.intentresolver.v2.validation.types.array import com.android.intentresolver.v2.validation.types.value @@ -61,8 +62,10 @@ internal fun Intent.maybeAddSendActionFlags() = addFlags(FLAG_ACTIVITY_MULTIPLE_TASK) } -fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) = - validateFrom(source) { +fun readChooserRequest(launch: ActivityLaunch): ValidationResult<ChooserRequest> { + val extras = launch.intent.extras ?: Bundle() + @Suppress("DEPRECATION") + return validateFrom(extras::get) { val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags() val isSendAction = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE) @@ -118,7 +121,7 @@ fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) = val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) - val referrerFillIn = Intent().putExtra(EXTRA_REFERRER, callerInfo.referrer) + val referrerFillIn = Intent().putExtra(EXTRA_REFERRER, launch.referrer) ChooserRequest( targetIntent = targetIntent, @@ -126,8 +129,8 @@ fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) = isSendActionTarget = isSendAction, targetType = targetIntent.type, launchedFromPackage = - requireNotNull(callerInfo.launchedFomPackage) { - "launchedFromPackage was null, See Activity.getLaunchedFromPackage()" + requireNotNull(launch.fromPackage) { + "launch.fromPackage was null, See Activity.getLaunchedFromPackage()" }, title = customTitle, defaultTitleResource = defaultTitleResource, @@ -146,6 +149,7 @@ fun readChooserRequest(callerInfo: CallerInfo, source: (String) -> Any?) = shareTargetFilter = targetIntent.toShareTargetFilter() ) } +} private fun Intent.toShareTargetFilter(): IntentFilter? { return type?.let { diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt index 663235ca..17b1e664 100644 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt +++ b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt @@ -18,7 +18,8 @@ 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.ActivityLaunch +import com.android.intentresolver.v2.ui.model.ActivityLaunch.Companion.ACTIVITY_LAUNCH_KEY import com.android.intentresolver.v2.ui.model.ChooserRequest import com.android.intentresolver.v2.validation.ValidationResult import dagger.hilt.android.lifecycle.HiltViewModel @@ -27,19 +28,15 @@ import javax.inject.Inject private const val TAG = "ChooserViewModel" @HiltViewModel -class ChooserViewModel -@Inject -constructor( - private val args: SavedStateHandle, -) : ViewModel() { +class ChooserViewModel @Inject constructor(args: SavedStateHandle) : ViewModel() { - private val callerInfo: CallerInfo = - requireNotNull(args[CallerInfo.SAVED_STATE_HANDLE_KEY]) { - "CallerInfo missing in SavedStateHandle! (${CallerInfo.SAVED_STATE_HANDLE_KEY})" + private val mActivityLaunch: ActivityLaunch = + requireNotNull(args[ACTIVITY_LAUNCH_KEY]) { + "ActivityLaunch missing in SavedStateHandle! ($ACTIVITY_LAUNCH_KEY)" } /** The result of reading and validating the inputs provided in savedState. */ - private val status: ValidationResult<ChooserRequest> = readChooserRequest(callerInfo, args::get) + private val status: ValidationResult<ChooserRequest> = readChooserRequest(mActivityLaunch) val chooserRequest: ChooserRequest by lazy { status.getOrThrow() } diff --git a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java index c0121f2e..37bbc6ce 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -54,13 +54,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance(); private UsageStatsManager mUsm; - // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at - // onCreate and needs to see some non-negative value in the test. - @Override - public int getLaunchedFromUid() { - return 1234; - } - @Override public ChooserListAdapter createChooserListAdapter( Context context, diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java index e7c8cce3..64c8e49d 100644 --- a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java @@ -65,13 +65,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW sOverrides.mWorkProfileAvailability); } - // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at - // onCreate and needs to see some non-negative value in the test. - @Override - public int getLaunchedFromUid() { - return 1234; - } - @Override public ChooserListAdapter createChooserListAdapter( Context context, diff --git a/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt b/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt new file mode 100644 index 00000000..d674bbc2 --- /dev/null +++ b/tests/activity/src/com/android/intentresolver/v2/ui/model/TestActivityLaunchModule.kt @@ -0,0 +1,41 @@ +/* + * 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.app.Activity +import android.net.Uri +import dagger.Module +import dagger.Provides +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped +import dagger.hilt.testing.TestInstallIn + +@Module +@TestInstallIn(components = [ActivityComponent::class], replaces = [ActivityLaunchModule::class]) +class TestActivityLaunchModule { + + @Provides + @ActivityScoped + fun activityLaunch(activity: Activity): ActivityLaunch { + return ActivityLaunch(activity.intent, LAUNCHED_FROM_UID, LAUNCHED_FROM_PACKAGE, REFERRER) + } + + companion object { + const val LAUNCHED_FROM_PACKAGE = "example.com" + const val LAUNCHED_FROM_UID = 1234 + val REFERRER: Uri = Uri.parse("android-app://$LAUNCHED_FROM_PACKAGE") + } +} diff --git a/tests/shared/src/com/android/intentresolver/v2/ext/ParcelableExt.kt b/tests/shared/src/com/android/intentresolver/v2/ext/ParcelableExt.kt new file mode 100644 index 00000000..3878c39c --- /dev/null +++ b/tests/shared/src/com/android/intentresolver/v2/ext/ParcelableExt.kt @@ -0,0 +1,45 @@ +/* + * 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.os.Parcel +import android.os.Parcelable +import java.lang.reflect.Field + +inline fun <reified T : Parcelable> T.toParcelAndBack(): T { + val creator: Parcelable.Creator<out T> = getCreator() + val parcel = Parcel.obtain() + writeToParcel(parcel, 0) + parcel.setDataPosition(0) + return creator.createFromParcel(parcel) +} + +inline fun <reified T : Parcelable> getCreator(): Parcelable.Creator<out T> { + return getCreator(T::class.java) +} + +inline fun <reified T : Parcelable> getCreator(clazz: Class<out T>): Parcelable.Creator<out T> { + return try { + val field: Field = clazz.getDeclaredField("CREATOR") + @Suppress("UNCHECKED_CAST") + field.get(null) as Parcelable.Creator<T> + } catch (e: NoSuchFieldException) { + error("$clazz is a Parcelable without CREATOR") + } catch (e: IllegalAccessException) { + error("CREATOR in $clazz::class is not accessible") + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt new file mode 100644 index 00000000..3e9f43da --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/ui/model/ActivityLaunchTest.kt @@ -0,0 +1,67 @@ +/* + * 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.Intent +import android.content.Intent.ACTION_CHOOSER +import android.content.Intent.EXTRA_TEXT +import android.net.Uri +import com.android.intentresolver.v2.ext.toParcelAndBack +import com.google.common.truth.Truth.assertWithMessage +import org.junit.Test + +class ActivityLaunchTest { + + @Test + fun testDefaultValues() { + val input = ActivityLaunch(Intent(ACTION_CHOOSER), 0, null, null) + + val output = input.toParcelAndBack() + + assertEquals(input, output) + } + + @Test + fun testCommonValues() { + val intent = Intent(ACTION_CHOOSER).apply { putExtra(EXTRA_TEXT, "Test") } + val input = + ActivityLaunch(intent, 1234, "com.example", Uri.parse("android-app://example.com")) + + val output = input.toParcelAndBack() + + assertEquals(input, output) + } + + fun assertEquals(expected: ActivityLaunch, actual: ActivityLaunch) { + // Test fields separately: Intent does not override equals() + assertWithMessage("%s.filterEquals(%s)", actual.intent, expected.intent) + .that(actual.intent.filterEquals(expected.intent)) + .isTrue() + + assertWithMessage("actual fromUid is equal to expected") + .that(actual.fromUid) + .isEqualTo(expected.fromUid) + + assertWithMessage("actual fromPackage is equal to expected") + .that(actual.fromPackage) + .isEqualTo(expected.fromPackage) + + assertWithMessage("actual referrer is equal to expected") + .that(actual.referrer) + .isEqualTo(expected.referrer) + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt index bcc1054c..29bb5cbd 100644 --- a/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestTest.kt @@ -16,11 +16,12 @@ package com.android.intentresolver.v2.ui.viewmodel import android.content.Intent +import android.content.Intent.ACTION_CHOOSER import android.content.Intent.ACTION_SEND import android.content.Intent.EXTRA_INTENT import androidx.core.net.toUri import androidx.core.os.bundleOf -import com.android.intentresolver.v2.ui.model.CallerInfo +import com.android.intentresolver.v2.ui.model.ActivityLaunch import com.android.intentresolver.v2.ui.model.ChooserRequest import com.android.intentresolver.v2.validation.RequiredValueMissing import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat @@ -30,18 +31,18 @@ import org.junit.Test @Suppress("DEPRECATION") class ChooserRequestTest { - private val callerInfo = - CallerInfo( - launchedFromUid = 10000, - launchedFomPackage = "com.android.example", + val intent = Intent(ACTION_CHOOSER) + private val mActivityLaunch = + ActivityLaunch( + intent, + fromUid = 10000, + fromPackage = "com.android.example", referrer = "android-app://com.android.example".toUri() ) @Test fun missingIntent() { - val args = bundleOf() - - val result = readChooserRequest(callerInfo, args::get) + val result = readChooserRequest(mActivityLaunch) assertThat(result).value().isNull() assertThat(result) @@ -51,13 +52,13 @@ class ChooserRequestTest { @Test fun minimal() { - val args = bundleOf(EXTRA_INTENT to Intent(ACTION_SEND)) + intent.putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND))) - val result = readChooserRequest(callerInfo, args::get) + val result = readChooserRequest(mActivityLaunch) assertThat(result).value().isNotNull() val value: ChooserRequest = result.getOrThrow() - assertThat(value.launchedFromPackage).isEqualTo(callerInfo.launchedFomPackage) + assertThat(value.launchedFromPackage).isEqualTo(mActivityLaunch.fromPackage) assertThat(result).findings().isEmpty() } } |