Start Activity from Fragment using Transition (API 21 support) - android

I'm trying to port an Android app to the new support library (support-v4:21.0.0) and I'm having trouble starting Activities from Fragments with a transition.
In my Activities, I've been doing something like:
Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle();
ActivityCompat.startActivityForResult(this, intent, REQUEST_SOMETHING, options);
which works fine for Activities. However, if I try to do something similar with Fragments, like:
Activity activity = getActivity();
Bundle options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity).toBundle();
ActivityCompat.startActivityForResult(activity, intent, REQUEST_SOMETHING, options);
it turns out that onActivityResult() is not called for the Fragment, but only the enclosing Activity. I haven't found anything in the support library to pass the options Bundle as a parameter to startActivityForResult() on an actual Fragment and have it call back to onActivityResult() in that Fragment. Is this possible?
The simplest solution would be to handle all onActivityResult() calls in the Activity itself, but I'd rather not do that because I have a ton of possible Fragments that may be receiving that callback.
Help is appreciated. Thanks!

Sadly, ActivityCompat.startActivityForResult() doesn't work quite right in Fragments (see Alex Lockwood's answer). For several weeks I marvelled at how Google never gave us an ActivityCompat method equivalent to Fragment's implementation of startActivityForResult(). What were they thinking?! But then I had an idea: Let's take a look at how the method is actually implemented.
As a matter of fact, startActivityForResult() in Fragment is different from the one in Activity (see here):
public void startActivityForResult(Intent intent, int requestCode) {
if (mActivity == null) {
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
mActivity.startActivityFromFragment(this, intent, requestCode);
}
Now startActivityFromFragment() looks like this (see here):
public void startActivityFromFragment(Fragment fragment, Intent intent,
int requestCode) {
if (requestCode == -1) {
super.startActivityForResult(intent, -1);
return;
}
if ((requestCode&0xffff0000) != 0) {
throw new IllegalArgumentException("Can only use lower 16 bits for requestCode");
}
super.startActivityForResult(intent,
((fragment.mIndex + 1) << 16) + (requestCode & 0xffff));
}
Google uses some odd byte shifting on the request code to make sure only the calling Fragment's onActivityResult() is called afterwards. Now since ActivityCompat doesn't provide any startActivityFromFragment(), the only option left is to implement it yourself. Reflection is required to access the package private field mIndex.
public static void startActivityForResult(Fragment fragment, Intent intent,
int requestCode, Bundle options) {
if (Build.VERSION.SDK_INT >= 16) {
if ((requestCode & 0xffff0000) != 0) {
throw new IllegalArgumentException("Can only use lower 16 bits" +
" for requestCode");
}
if (requestCode != -1) {
try {
Field mIndex = Fragment.class.getDeclaredField("mIndex");
mIndex.setAccessible(true);
requestCode = ((mIndex.getInt(this) + 1) << 16) + (requestCode & 0xffff);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
ActivityCompat.startActivityForResult(fragment.getActivity(), intent,
requestCode, options);
} else {
fragment.getActivity().startActivityFromFragment(this, intent, requestCode);
}
}
Copy that method anywhere you like and use it from your Fragment. Its onActivityResult() will be called as it should.
UPDATE:
Support library v23.2 was released and it seems startActivityFromFragment(Fragment fragment, Intent intent, int requestCode, Bundle options) does the job now :)

The ActivityCompat#startActivityForResult() method is just a proxy for the activity's startActivityForResult(Intent, Bundle) method. Calling the method from inside a fragment class doesn't mean that the Fragment's onActivityResult() will eventually be called as I'm sure you've found out. The framework has know way of knowing from which class the call originated... the only correct behavior would be to call the Activity's onActivityResult() method in this case.
It sounds like the best option would be to handle everything in the activity's onActivityResult() method as you suggested in your post.

You can create a listener interface or simply a public function in your fragment and passing the arguments from which you are getting in onActivityResult() of the activity to the listener or the public method of the fragment and do your desired work over there.

A simple way:
in Fragment:
ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation();
this.startActivityFromFragment(this, intent, requestCode, options);

Related

How to make onActivityResult get called on nested fragment from viewpager

This is a famous problem of getting result from activity to a nested fragment, the activity could send the result to only the Fragment that has been attached directly to Activity but not the nested one. That's the reason why onActivityResult of nested fragment would never been called no matter what.
In my case i have one activity that contains one fragment which has a viewPager with bunch of fragments.
In one of this fragments of the viewPager i try to start a camera intent, the result returned by this intent is never sent to the last fragment, i tried severals answers that i found but none of them is robust, one of the solutions was to create an EventBus object to diffuse the resultCode from activity to all the fragments but it doesn't work for this case, any help??
I might have a working solution for you since I used to be in a very similar situation.
In short terms I stopped relying on Fragment's onActivityResult() and implemented a working logic myself.
The only onActivityResult() you should trust is the one of your Activity.
You may create an interfacelike this one
public interface ActivityResultDispatcher {
// Pass anything you want, Bundle, Intent, ecc...
// For the sake of simplicity I'm using Bundle...
public void dispatchResultData(final Bundle data);
}
and have your Fragment classes implement it.
The communication between your Activity and your primary Fragmentshould be very easy that you can do something like this (in your Activity):
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
final Bundle bundle = new Bundle();
// put data inside Bundle
this.primaryFrament.dispatchResultData(bundle);
}
Now, your primary Fragment is the one that holds the ViewPager with the other Frament classes.
So in your primary Fragment you will do:
#Override public void dispatchResultData(final Bundle data) {
for (int i = 0; i < MyViewPager.NUM_FRAGMENTS; i++) {
final ActivityResultDispatcher subFragment = (ActivityResultDispatcher) this.retrieveFragment(i);
// some fragments in ViewPager may be dynamically deleted
if (subFragment != null) {
subFragment.dispatchResultData(data);
}
}
}
The function retrieveFragment(final int position) is a very cozy piece of code I had found on the web.
An implementation for a ViewPager works like this:
public Fragment retrieveFragment(final int position) {
return this.fragmentManager.findFragmentByTag("android:switcher:" + R.id.my_pager_id + ":" + position);
}
All your active Fragment classes in the ViewPager will receive the Bundle and you can decide what to do with it.
You may pass more information to the dispatcher, such as
public void dispatchResultData(final Bundle data, final int requestCode);
and have your Fragment decide if it should answer to the function call.
You have basically built a complete fully-working onActivityResult() utility and can be sure that all of your Fragments (if instantiated) will receive the result.

Get onActivityResult not on the same class send startActivityForResult

I try to make the speech recognize as show in this link https://software.intel.com/en-us/articles/developing-android-applications-with-voice-recognition-features.
It uses a run() method to make it thread on the background, which I call it from the fragment like this SpeechRecognitionHelper.run(getActivity());
Then in show it use
ownerActivity.startActivityForResult(intent,SystemData.VOICE_RECOGNITION_REQUEST_CODE);
where the ownerActivity is the getActivity() I pass it on the run().
So I was thinking I can implement the onActivityResult(..) in the Fragment(which call the run() and get the result on the fly.
But it is showing me a mistake when trying so.
Can't I get the result in the Fragment if I specify it even I use it in anther class?
If not how can I get the result if I use the run() method? How get the result if I have to use also the startActivityForResult?
onActivityResult() is the only way you can get the speech recognition results from the recognizer activity.
If you invoke it from Activity.startActivityForResult(), you have to override Activity.onActivityResult() on that same Activity subclass to get the result.
If you invoke it from Fragment.startActivityForResult(), you have to override Fragment.onActivityResult() on that same Fragment subclass to get the result.
Can't I get the result in the Fragment if I specify it even I use it in anther class?
From the link you shared startRecognitionActivity method expected the Activity and uses the Activity context to start the Voice Recognition Activity , so the result will be passed to the activity. Modify the method to start the Activity from Fragment context.
private static void startRecognitionActivity(Fragment callerFragment) {
// create an Intent with “RecognizerIntent.ACTION_RECOGNIZE_SPEECH” action
// start Activity and wait for result on fragment
callerFragment.startActivityForResult(intent, SystemData.VOICE_RECOGNITION_REQUEST_CODE);
}
Now override the onActivityResult on Fragment class
// Fragment Results handler
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// handle the result
if (requestCode == SystemData.VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) {
// Do your stuff here
}
}

bytearray in extra breaking return

Having a little trouble when trying to return a byte array from an activity. The return code is:
private void returnLocation(byte[] mapImage) {
Intent intent = new Intent();
intent.putExtra("mapImage", mapImage);
setResult(RESULT_OK, intent);
finish();
}
EDIT: This is how I call and try to receive it in the parent activity:
....
Intent i = new Intent(getActivity(), ChildActivity.class);
ParentFragment.this.startActivityForResult(i, 255);
...
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if(requestCode == 255) {
if(resultCode == Activity.RESULT_OK) {
mImage = data.getByteArrayExtra("mapImage");
}
}
}
I've a breakpoint on the onActiviyResult that never gets hit when i try returning a byte array.
However, when trying to return the intent it goes back to the first activity (not the one that called it). I can't see anything that suggests a problem in the logger.
If I change the Extra value to '5' (an integer), it returns fine.
Is there something I'm missing when trying to pass back a byte array?
Thanks
Android has a limit on the size of a Bundle, which is what the Intent uses internally. If the data exceeds the bundle size it will display ERROR/JavaBinder(7881): !!! FAILED BINDER TRANSACTION !! in the logs, but it won't throw an exception.
Try writing the byte array to a temp file and passing the file name in the Intent.
I figured that you are calling startActivityForResult(i, 255) from a Fragment.
Instead of calling ParentFragment.this.startActivityForResult(i, 255);
you should call : startActivityForResult(i, 255);
If the above approach doesnt work, try the following:
Since the Activity that is hosting your fragment gets onActivityResult() result so you need to override activity's onActivityResult(), call super.onActivityResult() in your fragment to propagate to respective fragment for unhandled results codes or for all.

PreferenceFragment doesn't get onActivityResult call from in app billing request

I have a preference screen that presents the user with a checkbox to disable ads. When the user clicks this for the first time, they are presented with an In App Billing purchase option to disable the ads.
The issue I'm facing here is that I can't see any way to get the onActivityResult callback into the fragment.
So I have a PreferenceActivity loading a PreferenceFragment (of which I can't seem to get a reference). In App Billing requires a call to startIntentSenderForResult, which Fragments don't have, only Activities.
When I launch the purchase flow with startIntentSenderForResult, the Activity's onActivityResult gets called, but I need this in the fragment.
Because I loaded the PreferenceFragment into the PreferenceActivity with the following, I don't think I can get a reference to the Fragment to pass the call down.
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.layout.preferences_headers, target);
}
#Override
public Intent getIntent() {
final Intent modIntent = new Intent(super.getIntent());
modIntent.putExtra(EXTRA_SHOW_FRAGMENT, SyncPreferencesFragment.class.getName());
modIntent.putExtra(EXTRA_NO_HEADERS, true);
return modIntent;
}
What am I missing here? I dont' want to split up all of my purchase logic, so how do I get my Fragment to get the onActivityForResult call?
The original start request must come from the Fragment in order for Android to deliver the result back to the same Fragment. If the Activity starts the request, the result doesn't inherently get handed to every Fragment that may be attached to the manager. In addition to this, you have to ensure that if onActivityResult() is overridden in the top-level Activity, that you call super.onActivityResult() as well, or the message won't get delivered to the Fragment.
The problem with IAB is your given a PendingIntent instead of a standard Intent to fire, and there is no method on Fragment to trigger the initial action even if you can move your code there. To keep things as they are, you may have to do a bit of a swizzle. One thing you could do is make use of a custom interface inside the onAttach() method of your Fragment and use it to allow the Fragment to hand itself up to the Activity. Something like:
public class SyncPreferencesActivity extends PreferenceActivity
implements SyncPreferencesFragment.OnAttachCallback {
SyncPreferencesFragment mTargetFragment;
#Override
public void onAttachSyncFragment(SyncPreferencesFragment fragment) {
mTargetFragment = fragment;
}
}
...with some additions to the corresponding Fragment...
public class SyncPreferencesFragment extends PreferenceFragment {
public interface OnAttachCallback {
public void onAttachSyncFragment(SyncPreferencesFragment fragment);
}
#Override
public void onAttach(Activity activity) {
try {
OnAttachCallback callback = (OnAttachCallback) activity;
callback.onAttachSyncFragment(this);
} catch (ClassCastException e) {
throw new ClassCastException("You Forgot to Implement the Callback!");
}
}
}
With something like this, at least you have a reference to the instance so you can forward the result or anything else that might be necessary. If you want to be super clean, you could also implement a matching "detach" that clears the reference in the Activity.
Here is a simpler way that I am using to get a reference to a Preference Fragment from a Preference Activity:
private WeakReference<GeneralPreferenceFragment> mGeneralFragment;
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
// "id" might be arbitrary and "tag" can be null
Log.d(TAG, "onAttachFragment: "+fragment.getId()+","+fragment.getTag());
// however class can be used to identify different fragments
Log.d(TAG, "onAttachFragment: "+fragment.getClass()+","+(fragment instanceof GeneralPreferenceFragment));
if(fragment instanceof GeneralPreferenceFragment) {
mGeneralFragment=new WeakReference<>((GeneralPreferenceFragment)fragment);
}
}
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Fragment f=mGeneralFragment.get();
if(f!=null) {
// can now use findPreference, etc
}
}
This method is convenient because it doesn't require the use of interfaces or modifications to the fragment classes.

Custom View calling startActivityForResult

I created custom compound view where I incorporate functionality to take pictures.
I'm calling it like this (from view):
Intent intent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
((Activity)mContext).startActivityForResult(intent, index);
This part works good. What I don't know how to do is how do I implement onActivityResult inside my custom view?
Or should I catch this inside Activity and than re-route into my view? Doesn't look like very nice solution..
You actually can do it like this:
#Override
public void onClick(View v) {
final FragmentManager fm = ((FragmentActivity) getContext()).getSupportFragmentManager();
Fragment auxiliary = new Fragment() {
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
//DO WHATEVER YOU NEED
super.onActivityResult(requestCode, resultCode, data);
fm.beginTransaction().remove(this).commit();
}
};
fm.beginTransaction().add(auxiliary, "FRAGMENT_TAG").commit();
fm.executePendingTransactions();
auxiliary.startActivityForResult(new Intent(getContext(), ToStartActivity.class), 3333);
}
The trick is using an auxiliary temp fragment.
I'm having the same issue, as the initial question. I know that you all posted working solution, BUT, all the solutions lack one thing: encapsulation. What do I mean - If in one activity I have 10 views that should (on some event) start another activity it should be NORMAL to be able to start that new activity from the view that needs that activity. You all are trying to convince that is better to handle all new possible activites from the initial one - than why we added different logic in each view. We may want to RE-USE code, and create one custom view that can work INDEPENDENT to where we use it (work may include showing another activity to select something for example).
I know that this is not possible (or not yet), and is a clear proof that Android SDK is not ready yet to handle real big applications.
If you want an example:in any real business app that has for example, customer list (that should be a view) ,the view should be able to launch by itself addcustomer activity, edit customer activity and so on, independent from where you put that customer list view (control) - because in big apps you need to RE-use components (you may need to show the customer list control in a order product activity, in a timesheet activity and so on.).
One possible solution could be:
- start the new activity (using the view context (normally should be the parent activity).
- on the new activity closing event, either call directly a method in the calling view (depending on the case, and posibilities: either static that is handling the code that you normally would run on activityresult, either try to pass the instance of the calling view to the new activity, and do the same. In this way, you can handle your new activity, without letting the containing activity to know anything about it.
You need to catch this from your activity. The startActivityForResult is called on your activity, so it'll be the one launching the Intent and getting the result. I'd say that it's overall bad to launch it directly from the view's code. A better solution would be with a clickListener (or checkChangeListener, or whatever you want), set by your activity, and calling a method like "openImageCapture".
When the Intent returns, your activity will take care of the result and update your views as needed.
Views are there just for displaying stuff on the screen and getting user input, the activity is there to do the actual work.
Here's a static function to implementing #riwnodennyk's solution, while overcoming the Fragment must be static and not in anonymous class error:
public static void myStartActivityForResult(FragmentActivity act, Intent in, int requestCode, OnActivityResult cb) {
Fragment aux = new FragmentForResult(cb);
FragmentManager fm = act.getSupportFragmentManager();
fm.beginTransaction().add(aux, "FRAGMENT_TAG").commit();
fm.executePendingTransactions();
aux.startActivityForResult(in, requestCode);
}
public interface OnActivityResult {
void onActivityResult(int requestCode, int resultCode, Intent data);
}
#SuppressLint("ValidFragment")
public static class FragmentForResult extends Fragment {
private OnActivityResult cb;
public FragmentForResult(OnActivityResult cb) {
this.cb = cb;
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (cb != null)
cb.onActivityResult(requestCode, resultCode, data);
super.onActivityResult(requestCode, resultCode, data);
getActivity().getSupportFragmentManager().beginTransaction().remove(this).commit();
}
}
Usage example:
Intent inPhonebook = new Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI);
myStartActivityForResult((FragmentActivity) getContext(),
inPhonebook, REQUEST_CODE_PICK_CONTACT, this::onContacts);
There is no way to catch onActivityResult from your view, only from Activity.
And its not safe to assume that's Context object is Activity. In general you should not rely on this fact. Even if it seems reasonable in case with views, you still should use only methods available trough Context interface. That's because your can't predict all side-effects on the Activity, when you're calling Activity specific functions.
Just make the same method inside your custom view
And inside the activitys onActivityResult call yourView.onActivityResult(...) and process the result inside your view..
Also as guys mentioned you must not always end up with Context being of Activity class. Usually when it is from inflated view.
But if you construct your view only in code and always use the activity instance you are good.

Categories

Resources