How to implement callbacks for intents in Xamarin.Android - android

I tried to implement a platform-independent wrapper for sharing files that takes a filename and an Action as a callback. Despite it does work, I am not content with my Android solution at all.
What I am currently doing is: I am registering my callback with my MainActivity, which returns an ID which I can use to identify the callback
int callbackId = MainActivity.Instance.RegisterCallback(completionHandler);
Then I start ActionShare intent with
MainActivity.Instance.StartActivityForResult(Intent.CreateChooser(intent, "Select App"), 123 | (callbackId << 8));#
The 123 is my identifier for the action.
In my MainActivity in OnActivityResult I am getting and calling my callback with
var callbackId = (requestCode & 0b1111111100000000) >> 8;
if (this.RemoveCallbackById(callbackId, out var callback))
{
callback();
}
Is there a better way implementing callbacks for intents in Xamarin.Android? I believe there must be, but being constrained to passing only a single 16 bit integer to StartActivityForResult that will be passed to OnActivityResult the possibilities seem quite limited to me.

Related

How do I set up assistant shortcut suggestions with actions.intent.OPEN_APP_FEATURE?

So I'm just wondering why my code isn't working. How do I give AppShorcutIntent a specific intent with an action and data and stuff like that?
This is my code so far:
val appShortcutIntent = AppShortcutIntent.builder()
.setIntentName("actions.intent.OPEN_APP_FEATURE")
.setPackageName("com.app.name")
.setIntentParamName("feature")
.setIntentParamValue("")
.build()
shortcutsClient.lookupShortcut(appShortcutIntent)
.addOnSuccessListener { shortcutLookupResult ->
if (shortcutLookupResult.isShortcutPresent) {
shortcutsClient.createShortcutSettingsIntent().addOnSuccessListener { intent ->
requireActivity().startActivity(intent)
}
return#addOnSuccessListener
}
val signalShortcut = AppShortcutSuggestion.builder()
.setAppShortcutIntent(appShortcutIntent)
.setCommand("feature on")
.build()
shortcutsClient.createShortcutSuggestionIntent(signalShortcut).addOnSuccessListener { intent ->
requireActivity().startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
I have tried so many different things and none of it seems to want to work the way I want. I know the question doesn't have anything specific as the parameter value but no matter what I set the param value too it still just doesn't get recognized as a unique intent when I use the shortcut.
The in-app promo library API doesn't deal with Android intents. It deals with Assistant's built-in intents, which are an entirely different things (even though they are both called "intents"). In the example you copied, it refers to the BII called OPEN_APP_FEATURE.
By using this API, you are telling Assistant how to create a shortcut that launches the app using a BII that it is already configured to handle. This BII is important because it carries the ability to recognize natural language queries associated with it. Android intents don't have that context.

startActivityForResult crashes while startActivity works

I have an activity that I'm starting:
Intent intent = new Intent(this, CreateItemDetailsActivity.class);
if(storeId != null) {
intent.putExtra(Identifiers.STORE_ID, storeId);
}
intent.putExtra(Identifiers.ITEM_NAME, name);
intent.putExtra(Identifiers.ITEM_DESCRIPTION, description);
startActivity(intent);
This works, but now I need to return data to the original activity, so I change startActivity call to:
startActivityForResult(intent, CREATE_ITEM_RESULT);
(CREATE_ITEM_RESULT is just a random integer number 63463657 I made up)
However, my app crashes without neither onCreate methods (I've implemented and put a breakpoint in both) being called:
Uncaught exception in thread main: java.lang.IllegalStateException: Could not execute method for android:onClick
android.support.v7.app.AppCompatViewInflater$DeclaredOnClickListener.onClick(AppCompatViewInflater.java:389)
(the code is called in a button click handler, hence onClick)
I've seen App crashes when calling startActivityForResult which has the same problem but the accepted answer suggests removing a line, recipe.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);, which I don't have.
Why am I getting a crash on startActivityForResult while startActivity works perfectly?
(My min SDK level is 16, target and compile SDK levels are 27, and I'm running Android 8.1 emulator)
If have not seen any documentation on this issue, but try a four digit int value should get rid of the error.
I have read Google docs for startActivityForResult and I have not found any reference to a maximum int size. Even the code documentation states:
#param requestCode If >= 0, this code will be returned in the
onActivityResult() when the activity exists
So, no mention of a maximum int value. Odd...
EDIT
Thanks to #MidasLefko for finding this information:
RequestCodes can only be a max of 0xffff (65535). So you are probably
calling startActivityForResult(intent, REQUEST_CODE); and REQUEST_CODE
is greater than 65535.
on this website:
https://code-examples.net/en/q/db5947
In the non-support Activity class implementation of startActivityForResult there is no restriction on the maximum int for requestCode, as long as it is positive. However FragmentActivity (and by extension AppCompatActivity) overrides this behavior (documentation):
void startActivityForResult (Intent intent, int requestCode)
Modifies the standard behavior to allow results to be delivered to fragments. This imposes a restriction
that requestCode be <= 0xffff.
In the source code of FragmentActivity one finds the following helpful comment:
A hint for the next candidate request index. Request indicies are ints
between 0 and 2^16-1 which are encoded into the upper 16 bits of the
requestCode for Fragment.startActivityForResult(...) calls. This
allows us to dispatch onActivityResult(...) to the appropriate
Fragment. Request indicies are allocated by allocateRequestIndex(...).
ints in java are 32 bit, FragmentActivity uses 16 to determine which fragment to send the result to and the other 16 are available for the developer.

DevicePolicyManager.enableSystemApp() method does not work as expected

I'm trying to use enableSystemApp method to activate default system apps after provisioning device with the app that is set to device owner mode.
There are two methods to do this:
1) void enableSystemApp (ComponentName admin, String packageName) - in this case you need to pass package name explicitly as String. It works fine, the app gets enabled.
For example, calling this
devicePolicyManager.enableSystemApp(deviceAdminComponent, "com.google.android.gm");
enables default Gmail client, which is disabled after provisioning.
2) int enableSystemApp (ComponentName admin, Intent intent) - in this case, you need to pass an implicit intent and Android should enable all system apps that match this intent. In addition, this method returns int number of apps that match the intent. And here's the problem - I can't get this method to work, it always returns 0 and doesn't enable anything.
Here's the snippet I'm trying to use:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_EMAIL);
int i = devicePolicyManager.enableSystemApp(deviceAdminComponent, intent);
It does not work and i == 0 in this case. What am I doing wrong?
Any help is appreciated!
Under the hood, the method that accepts an intent queries to get the list of activities that respond to that intent and then loops through the list passing in the package name string to enable the package. It's similar to doing this:
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_APP_EMAIL);
List<ResolveInfo> infoes = getPackageManager()
.queryIntentActivities(intent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE);
for (ResolveInfo info in infoes) {
devicePolicyManager.enableSystemApp(deviceAdminComponent, info.activityInfo.packageName);
}
Since you are able to enable the app using the package name string, the fault most likely lies in the way the intent is being resolved - which is supported by the fact that it always returns 0.
It is counter-intuitive, but my suspicion is that the application does not resolve the ACTION_MAIN intent because the app is disabled. Have you tried a less generic intent? I would try the following
Intent i;
// #1
// This goes full circle, but I expect it should work
i = getPackageManager().getLaunchIntentForPackage("com.google.an‌​droid.gm")
// #2
i = new Intent(Intent.ACTION_SEND).setPackageName("com.google.android.gm");
// #3
// Generic, but should resolve _all_ email apps - not just the default one.
// The mailto schema filters out non-email apps
i = new Intent(Intent.ACTION_VIEW , Uri.parse("mailto:"));
Option #1 and #2 are more academic. Both require the package name at which point you may as well use the string overload of enableSystemApp. Option #3 is my best guess for something generic that might still work, but it's possible that it still won't work because the app is disabled.
Note: I find it interesting that enableSystemApp only passes the MATCH_DIRECT_BOOT_AWARE and MATCH_DIRECT_BOOT_UNAWARE flags when querying activities that can resolve the intent, because the MATCH_DISABLED_COMPONENTS and MATCH_SYSTEM_ONLY flags seem much more relevant in this situation.

How can I test setResult() in an Android Espresso test?

Is there any good way to test the result code and data in an Android Espresso test? I am using Espresso 2.0.
Suppose I have an Activity called BarActivity.class, which upon performing some action, calls setResult(int resultCode, Intent data) with the appropriate payload.
I'd like to write a test case to verify the resultCode and data. However, because setResult() is a final method, I can't override it.
Some options I thought about were:
Define a new method like setActivityResult() and just use that so it can be intercepted, etc...
Write a test-only TestActivity that will call startActivityForResult() on BarActivity and check the result in TestActivity.onActivityResult()
Trying to think what's lesser of the two evils, or if there's any other suggestions on how to test for this. Any suggestions? Thanks!
If meanwhile you switched to the latest Espresso, version 3.0.1, you can simply use an ActivityTestRule and get the Activity result like this:
assertThat(rule.getActivityResult(), hasResultCode(Activity.RESULT_OK));
assertThat(rule.getActivityResult(), hasResultData(IntentMatchers.hasExtraWithKey(PickActivity.EXTRA_PICKED_NUMBER)));
You can find a working example here.
If you are willing to upgrade to 2.1, then take a look at Espresso-Intents:
Using the intending API (cousin of Mockito.when), you can provide a response for activities that are launched with startActivityForResult
This basically means it is possible to build and return any result when a specific activity is launched (in your case the BarActivity class).
Check this example here: https://google.github.io/android-testing-support-library/docs/espresso/intents/index.html#intent-stubbing
And also my answer on a somewhat similar issue (but with the contact picker activity), in which I show how to build a result and send it back to the Activity which called startActivityForResult()
this works for me:
#Test
fun testActivityForResult(){
// Build the result to return when the activity is launched.
val resultData = Intent()
resultData.putExtra(KEY_VALUE_TO_RETURN, true)
// Set up result stubbing when an intent sent to <ActivityB> is seen.
intending(hasComponent("com.xxx.xxxty.ActivityB")) //Path of <ActivityB>
.respondWith(
Instrumentation.ActivityResult(
RESULT_OK,
resultData
)
)
// User action that results in "ActivityB" activity being launched.
onView(withId(R.id.view_id))
.perform(click())
// Assert that the data we set up above is shown.
onView(withId(R.id.another_view_id)).check(matches(matches(isDisplayed())))
}
Assuming a validation like below on onActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) {
data?.getBooleanExtra(KEY_VALUE_TO_RETURN, false)?.let {showView ->
if (showView) {
another_view_id.visibility = View.VISIBLE
}else{
another_view_id.visibility = View.GONE
}
}
}
I follow this guide as reference : https://developer.android.com/training/testing/espresso/intents and also i had to check this links at the end of the above link https://github.com/android/testing-samples/tree/master/ui/espresso/IntentsBasicSample
and
https://github.com/android/testing-samples/tree/master/ui/espresso/IntentsAdvancedSample
If you're using ActivityScenario (or ActivityScenarioRule) as is the current recommendation in the Android Developers documentation (see the Test your app's activities page), the ActivityScenario class offers a getResult() method which you can assert on as follows:
#Test
fun result_code_is_set() {
val activityScenario = ActivityScenario.launch(BarActivity::class.java)
// TODO: execute some view actions which cause setResult() to be called
// TODO: execute a view action which causes the activity to be finished
val activityResult = activityScenario.result
assertEquals(expectedResultCode, activityResult.resultCode)
assertEquals(expectedResultData, activityResult.resultData)
}

Android custom keyboard for sending images

I am currently trying to implement a custom keyboard that sends an image (possibly via an intent?) to a certain application. More specifically, I am trying to I am trying to create a key on a custom keyboard that would send an image to the default messaging app so that it can be sent as an MMS.
I have modified the sample SoftKeyboard project in order to do this. Here is what I have so far:
private void handleCharacter(int primaryCode, int[] keyCodes) {
if (isInputViewShown()) {
if (mInputView.isShifted()) {
primaryCode = Character.toUpperCase(primaryCode);
}
}
if (isAlphabet(primaryCode) && mPredictionOn) {
mComposing.append((char) primaryCode);
// Send message here
Intent pictureMessageIntent = new Intent(Intent.ACTION_SEND);
pictureMessageIntent.setType("image/png");
Uri uri = Uri.parse("android.resource://" + this.getPackageName() + "/drawable/icon_en_gb");
pictureMessageIntent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(pictureMessageIntent);
getCurrentInputConnection().setComposingText(mComposing, 1);
updateShiftKeyState(getCurrentInputEditorInfo());
updateCandidates();
} else {
getCurrentInputConnection().commitText(
String.valueOf((char) primaryCode), 1);
}
}
The problem is I am getting a runtime exception that says:
android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?
I'm unfamiliar with Android custom keyboards, but I'm not sure if starting a new Activity is the best idea. Does anyone have any suggestions?
The problem is that Activities are typically meant to be started from other Activities. Android added this error to make sure that developers only change this flow willingly and consciously.
Add the following line before sending the intent to fix the problem:
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
You need to do exactly what it says- add the FLAFG_ACTIVITY_NEW_TASK flag to the intent. Any intent from a service needs that. Fix that and it should work (or at least start the activity).
Put this line:
pictureMessageIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
as you are creating this keyboard for sending images from uri, then add this line too:
// To get images from uri
pictureMessageIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Before the line:
startActivity(pictureMessageIntent);

Categories

Resources