So I've got to the point where my Android Activities and Fragments are getting a little messy. And I'd like to apply some good coding practice. However, every time I come up with a good SOLID design, Android gets in the way!
As a concrete example, I have a fragment that allows a user to update their details (name, email etc). It also allows them to take a picture to use as their profile picture.
I want to move this "picture taking" code out into a separate interface. Let's call it PhotoTaker. I then want a class called AndroidPhotoTaker that will implement this interface to start the correct Intent and return the filename of the picture taken.
public interface PhotoTaker {
Uri capturePhoto();
}
public class AndroidPhotoTaker implements PhotoTaker {
private Context _context;
#Inject
public AndroidPhotoTaker(Context context) {
_context = context;
}
#Override
public Uri capturePhoto() {
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (takePictureIntent.resolveActivity(_context.getPackageManager()) != null) {
File photoFile = new File("some/path/to/a/file");
Uri photoUri = FileProvider.getUriForFile(_context, _context.getApplicationContext().getPackageName() + ".provider", photoFile);
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
_context.startActivity(takePictureIntent);
return photoUri;
}
return null;
}
}
Assume that the correct context gets injected in via Dagger2.
My problem is that I can no longer call startActivityForResult() as I did in the fragment. And thus I no longer have the onActivityResult() callback to process the result. The only thing I can really find is _context.startActivity(), but that doesn't seem right.
And there's no callback -- I want to be able to get any extra data from these intents in the future.
How have other people solved this problem? What's the general solution here? i.e. how do people abstract away Android intent calls that have callbacks?
In my opinion abstracting away the calback handling makes the coding far too complicated: you have to implement your own
Activity-Baseclass that catches calls that android makes to onActivityResult() and forward these calls to your self-defined
IOnActivityResult interface that your hosting activity must implement.
Instead of this you can create a helper class with static method that creates and analyses Intents.
public class MyPhotoTakerClientActivity extends Activity {
private void onTakePhotoClicked() {
Intent takePictureIntent = PhotoTakerHelper.createIntent("some/path/where/to/store/the/resultfile.jpg");
if (PhotoTakerHelper.canProcess(this, takePictureIntent)) {
startActivityForResult(takePictureIntent, TakePhotoID);
} else {
... error processing
}
}
protected void onActivityResult(final int requestCode,
final int resultCode, final Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
switch (requestCode) {
case TakePhotoID :
Uri uri = PhotoTakerHelper.getPhotoUri(intent);
showPhoto(uri);
break;
...
In my apps all Activities have a static method showActivity that receives all parameters that can travel from the calling activity to sub-activity:
public class FotoGalleryActivity extends Activity {
public static void showActivity(Activity context, GalleryFilterParameter filter,
QueryParameter query, int requestCode) {
Intent intent = new Intent(context, FotoGalleryActivity.class);
if (filter != null) {
intent.putExtra(EXTRA_FILTER, filter.toString());
}
if (query != null) {
intent.putExtra(EXTRA_QUERY, query.toReParseableString());
}
if (requestCode != 0) {
context.startActivityForResult(intent, requestCode);
} else {
context.startActivity(intent);
}
}
}
The calling activity does not need to know how the parameters travel. All it needs is a call to FotoGalleryActivity.showActivity(getActivity(), getFilter(), null, ID_GALLERY);
Related
My searches did not return an answer, but one or two unanswered questions. I'll try to phrase it in a way that maybe I get an answer.
TL;DR version:
I cannot figure our how it is possible to launch a (VPN) service from AndroidViewModel class.
Longer version:
Normally, from an activity I would launch a service like this:
#Override
public void onClick(View v) {
Intent intent = VpnService.prepare(this);
if (intent != null) {
startActivityForResult(intent, 0);
} else {
onActivityResult(0, RESULT_OK, null);
}
}
Obviously, in MVVM I want to pass on the click event to the AndroidViewModel (or simply ViewModel, but I need Context, so AndroidViewModel it has to be), and there I want to launch the service.
Am I supposed to do it like this? It seems messy.
public void startStopButtonWasClicked(WeakReference<Activity> MainActivity) {
//do other stuff
Intent intent = VpnService.prepare(MainActivity.get());
if (intent != null) {
application.startActivity (intent, 0);
MainActivity.get().startActivityForResult(intent, 0);
} else {
MainActivity.get().onActivityResult(0, RESULT_OK, null);
}
}
Or should I implement onActivityResult in the VM class?
Edit: this is not even possible: MainActivity.get().onActivityResult(0, RESULT_OK, null);
You don't navigate from your viewmodel, you navigate from your view/controller, send a command to your controller saying you want to start a service and start it from your controller.
By the way getting your activity context like this : MainActivity.get() is really bad, never put a activity context in a static member.
Or should I implement onActivityResult in the VM class?
Indeed when you get your result from your controller you can send it to your viewmodel to handle it
First I think I should describe my problem:
I want to do a BluetoothRequest for turning it on, but I don't want to create a new Activity. I want to keep it in the MainActivity and put the code in subclasses. How is this possible? Down is my own idea of how to do it, but it don't works.
I have the MainActivity and a BluetoothActivity. The following code is from BluetoothActivity:
public class Bluetooth extends Fragment{
private Context context;
private TextView textView;
private final int REQUEST_ENABLE_BT = 1;
private String BtName;
private String BtAddress;
private BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
public Bluetooth(Context context){
//constructor
this.context=context;
textView = new TextView(this.context);
}
public boolean bluetoothUnterstuetz(){
if(mBluetoothAdapter == null) {
//Device doesn't support Bluetooth
textView.setText("Device doesn't support Bluetooth");
}
return true;
}
public void bluetoothActivate(){
if(mBluetoothAdapter.getState()== BluetoothAdapter.STATE_ON){
textView.setText("Bluetooth already on.");
}
if(!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
((Activity) context).startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
((Activity) context).setContentView(textView);
}
public void onActivityResult(int requestCode, int resultCode, Intent data){
super.onActivityResult(requestCode, resultCode, data);
textView.setText(resultCode);
Log.i("bluetooth", "called");
((Activity) context).setContentView(textView);
}
With extends Fragment nothing happens, also logcat is empty. With extends Activity, the app crashs after starting.
When I test the App on my Phone, I get the BluetoothRequest and can Choose my answer but afterwards the ContentView shows nothing, also Logcat stays empty
Somebody knows how to get the Result?
Well now it works.
The onActivityResult() has to be in the MainActivity. If I put it into the subclass it won't be called
You have forgotten tag override.
#Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
}
AndroidStudio tells me the function onActivityResult is never used.
Using the annotation #Override will make this warning to disappear. It is also a good way to check you overrided properly (with the right parameters in the right order) as you will get an error should the override be wrong. Which doesn't look to be the case here if you extended Activity.
When I test the App on my Phone, I get the BluetoothRequest and can Choose my answer but afterwards the ContentView shows nothing.
Are you sure your startActivityForResult() was called and returned a result (ie. didn't call finish() for example)? I'll update my answer according to the details you will provide.
onActivityResult is a protected method of Activity and a public method of Fragment. If ide is warning you that the method is never used is because your class does not extend neither Activity nor Fragment
I have a method that needs to be called from another activity, and I need to use it to set an ImageView in it's own activity. I have this method in MainActivity:
public static void setImageView(String fileName){
Log.i(TAG, fileName);
imageView = (ImageView) findViewById(R.id.imageView);
bmp = BitmapFactory.decodeFile(fileName);
imageView.setBackgroundResource(0);
imageView.setImageBitmap(bmp);
}
But I can't make a static reference to findViewById because it isn't a static method. This method is being called in a Camera Activity after the photo has been saved, I want to pass in the fileName (file URI) and set the imageView such that when the Camera Activity finishes and the user return to the MainActivity the ImageView is already set. As such, in CameraView I am trying to call this:
...code...
mCamera.takePicture(null, null, callback);
MainActivity.setImageView(fileName);
Is there a cheeky way around this? I know there are other posts on this but I can't quite work out how to apply the advice given there to my situation.
Thanks!
Maybe you should launch your CameraActivity using startActivityForResult() method, and after take the photo put the fileName as an Extra into an Intent and set it as result. Then in your MainActivity you can get the fileName back from the Intent arg of onActivityResult().
Something like:
public class MainActivity extends Activity{
public void aMethod(){
...
Intent i = new Intent(this, CameraActivity.class);
startActivityForResult(i, REQUEST_CODE);
...
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
Bundle b = data.getExtras();
String fileName = b.getString(RETURN_FILE_PARAMETER);
doSomething(fileName);
}
}
}
...
public class CameraActivity extends Activity{
private void returnFileFinishActivity(String fileName) {
Intent retIntent = new Intent();
retIntent.putExtra(RETURN_FILE_PARAMETER, fileName);
setResult(RESULT_OK, retIntent);
finish();
}
}
Regards.
A few things.
1) Since you should only be displaying one activity at a time, why not just start with startActivityForResult in the activity with the ImageView and override onActivityResult in your camera activity?
2) I'm not sure of your application, but it may be easier if you implement taking a picture by following this: http://mobile.tutsplus.com/tutorials/android/android-sdk-quick-tip-launching-the-camera/ .
3) You can expose a static reference to your main activity, in your main activity's onCreate method, do something like staticRef = this; and in your camera activity simply access it via MainActivity.staticRef... (I would not recommend this approach)
4) You can register a broadcast receiver in your Main activity that has a reference to your main activity or image view and in your camera activity you send a broadcast to it which you can set the image view
my onActivityResult method is never called. am using android 2.2
I am using a Tabhost, where TabHosts contain TabGroups which contain individual Activities.
One of my individual activity runs the following intent
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,
"Select Picture"), 0);
this loads my gallery apps, I use the default android gallery to select one image and when I return my onActivityResult is not called my activity.
It looks like this - and I put a breakpoint at if(resultCode == 0) , so right now, the logic of my onActivityResult should not matter
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == 0) {
if (requestCode == 0) {
Uri selectedImageUri = data.getData();
//OI FILE Manager
filemanagerstring = selectedImageUri.getPath();
//MEDIA GALLERY
selectedImagePath = getPath(selectedImageUri);
//DEBUG PURPOSE - you can delete this if you want
if(selectedImagePath!=null)
System.out.println(selectedImagePath);
else System.out.println("selectedImagePath is null");
if(filemanagerstring!=null)
System.out.println(filemanagerstring);
else System.out.println("filemanagerstring is null");
//NOW WE HAVE OUR WANTED STRING
if(selectedImagePath!=null)
System.out.println("selectedImagePath is the right one for you!");
else
System.out.println("filemanagerstring is the right one for you!");
}
}
}
Lifecycle functions are often called out of order and intermittently for Activities within a tabhost/tabgroup, so I checked to see what lifecycle functions ARE being called after the gallery closes (this happens as soon as I select an image from the android gallery)
The only one being called is the onResume() in my TabHost activity. So I tried putting the exact same onActivityResult() method in my TabHost class AS WELL AS the TabActivity class. With a breakpoint in the same location at the beginning of method.
Neither of these classes are called.
I'm drawing a blank now, how can I get the result from the gallery app in my app if none of the built in receiving methods will respond to it.
Since I know that my main TabHost gets the onResume() called, I tried added Intent graphics = getIntent(); to see if it would receive data from the gallery selection, it does not, so I don't see how I can do the logic in the onResume() method either.
Solutions welcome! :)
Try to call the startActivityForResult using the context of the tabgroup activity containing your current activity and then listen in the tabgroup activity.
Use this to get the tabGroupActivity:
TabGroupActivity parentActivity = (TabGroupActivity)getParent();
And then call startActivityForResult from it:
parentActivity.startActivityForResult(...);
Finally , put an onActivityResult listener in the tabGroupActivity:
protected void onActivityResult(int requestCode, int resultCode,Intent intent) {
super.onActivityResult(requestCode, resultCode, intent);
}
Judging from the many questions like this one, there are many reasons why a called activity may not trigger the caller's onActivityResult() method.
One reason I found, was when I called startActivityForResult(intent, requestCode), with a requestCode value of less than 0. My application did not need a requestCode and the Android documentation said using < 0 would not send a requestCode.
But the Android docs did not mention the consequence of a requestCode < 0. The consequence is that it prevents the caller's onActivityResult() method from ever being invoked! Ouch!
Therefore, even if your app does not need a requestCode, you many still want to use one with a value >= 0.
That's what I learned today:-)
The solution is to call a transparent activity over top of the main activity. This transparent activity is in front of the tabhost and will have normal lifecycle functions.
This transparent activity calls the gallery intent onCreate(), it gets everything returned like normal in its onActivityResult and you will be able to pass the information returned back to the rest of the app like normal. finish() is inside of the onActivityResult method, so the user never even notices that a transparent activity was called.
Update copied from from comments:
Activity A calls Activity B via normal intent. Activity B has no xml and runs onCreate like this
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//setContentView(R.layout.dialogpopper);
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*"); startActivityForResult(intent, 0);
}//end onCreate
and when Activity C is finished it calls the onActivityResult of Activity B
You just have to remove android:noHistory="true" this form your manifest file.
Use the constant values for the Result codes:
Activity.RESULT_OK and
Activity.RESULT_CANCELED
You'll see that the value for cancelled is actually 0. So in your code you are checking to see if the activity was cancelled.
change your code to
if (resultCode == Activity.RESULT_OK) {
...
}
Additionally change your Intent action to be:
intent.setAction(Intent.ACTION_PICK);
If you do this, you can just call
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
startActivityForResult(intent, 0);
instead of creating the chooser. It will automatically pick the activities associated with that intent and mimetype and display them to you
The way onActivityResult is called depends on the launchMode of your Activity (in the manifest). I'm not sure if that can be an issue here.
do you have #Override above your onActivityRestult?
(looking at old code that does this so not sure why its needed) call super.onactivityresult(requestcode, resultscode, data) as the first call in the method
also my intents didnt have that other stuff in them
startActivityForResult(Intent.createChooser(intent,
"Select Picture"), 0);
i think should just be
startActivityForResult(source.class, destination.class);
of course source and destination should be the name of the classes
public class ImageSwitcherView extends Activity {
int pics[] = { R.drawable.image000, R.drawable.image001,
R.drawable.image002};
private int currentIndex = 0;
SharedPreferences preferences;
Gallery gallery;
ImageView fullPicView;
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.galleryview);
Bundle extras = getIntent().getExtras();
currentIndex = extras.getInt("bookmark");
gallery = (Gallery) findViewById(R.id.Gallery01);
gallery.setAdapter(new ImageAdapter(this));
gallery.setSelection(currentIndex);
gallery.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView parent, View v, int position,
long id) {
currentIndex = position;
// ---display the images selected---
fullPicView = (ImageView) findViewById(R.id.imageView1);
fullPicView.setImageResource(pics[currentIndex]);
fullPicView.setOnClickListener(new OnClickListener() {
#Override
public void onClick(View v) {
// TODO Auto-generated method stub
Intent myIntent = new Intent(ImageSwitcherView.this,
imageView.class);
int resID = pics[currentIndex];
myIntent.putExtra("resID", resID);
myIntent.putExtra("index", currentIndex);
startActivityForResult(myIntent, 1);
}
});
}
});
}
public class ImageAdapter extends BaseAdapter {
private Context context;
private int itemBackground;
public ImageAdapter(Context c) {
context = c;
// ---setting the style---
TypedArray a = obtainStyledAttributes(R.styleable.Gallery1);
itemBackground = a.getResourceId(
R.styleable.Gallery1_android_galleryItemBackground, 0);
a.recycle();
}
// ---returns the number of images---
public int getCount() {
return pics.length;
}
// ---returns the ID of an item---
public Object getItem(int position) {
return position;
}
public long getItemId(int position) {
return position;
}
// ---returns an ImageView view---
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView = new ImageView(context);
imageView.setImageResource(pics[position]);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
imageView.setLayoutParams(new Gallery.LayoutParams(150, 120));
imageView.setBackgroundResource(itemBackground);
return imageView;
}
}
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
currentIndex = data.getIntExtra("bookmark", 0);
gallery.setSelection(currentIndex);
fullPicView.setImageResource(pics[currentIndex]);
}
#Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK
|| keyCode == KeyEvent.KEYCODE_HOME) {
preferences = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = preferences.edit();
editor.putInt("bookmark", gallery.getSelectedItemPosition());
editor.commit();
finish();
}
return super.onKeyDown(keyCode, event);
}
}
When using Android startActivityForResult, I don't have any guarantee about what I'll get in the Intent returned by onActivityResult.
I would like to define some kind of interface to limit the possibility of errors when transmitting data from an Activity to another (eg mistyped variable name).
Is there a way to do that? For example could I use something similar to the Android Interface Definition Language but between Activitys?
There are 2 scenarios when passing data between two activities A,B.
Activity A wants to pass data on Activity B ( through the startActivity Intent )
Activity B wants to return data on Activity A when it ends using setResult
on both cases i suggest to create some public static final variables for the extra keys to use.
For example if you need to pass an integer using the key "rating" from A to B i would probably do
class A extends Activity {
public static final String RESULT_STATUS = "RESULT_STATUS";
// Whatever ....
public void startB(int rating) {
Intent toStart = new Intent(this, B.class);
toStart.putExtra(B.EXTRA_RATING, rating);
startActivityForResult(toStart, 0);
}
public void onActivityResult(int requestCode /* 0 in our case */, int resultCode, Intent data) {
if (resultCode == RESULT_OK ) {
String returnedStatus = data.getStringExtra(RESULT_STATUS);
// Whatever ....
}
}
class B extends Activity {
public static final String EXTRA_RATING = "EXTRA_RATING";
public void onCreate(Bundle b) {
// Whatever ....
int rating = getIntent().getIntExtra(EXTRA_RATING,0);
}
// Whatever ....
public void returnDataAndFinish(String status) {
Intent result = new Intent();
result.putExtra(A.RESULT_STATUS, status);
setResult(RESULT_OK, result);
finish();
}
}