Getting robolectric to work with android compatibility package - android

I try to use Robolectric to run tests for my Android application, which is using the android-support package. In my onCreate() method I call
getSupportLoaderManager().initLoader(0, null, this);
Unfortunately, getSupportLoaderManager() returns null here. Do I have to prepare anything to make the compatiblity classes available to robolectric?
I have the android.jar (android-8) and the android-support-v4.jar in the classpath in my test project, along with robolectric-1.2 snapshot version.
Test class:
#RunWith(RobolectricTestRunner.class)
public class HoebAppActivityTest {
#Test
public void shouldStartActivity() {
final MyActivity activity = new MyActivity();
System.out.println(activity.getSupportLoaderManager()); // return null
activity.onCreate(null);
}
}
Edit:
System.out.println(Robolectric.directlyOnFullStack(activity)
.getSupportLoaderManager());
outputs android.support.v4.app.LoaderManagerImpl#9a90b9 so I guess, I just need to feed this back to the activity? Seems like I am getting closer here.
Edit2: I tried to bind my own ShadowFragmentActivity:
#Implements(FragmentActivity.class)
public class ShadowFragmentActivity extends
com.xtremelabs.robolectric.shadows.ShadowFragmentActivity {
#RealObject
private FragmentActivity realActivity;
#Implementation
public LoaderManager getSupportLoaderManager() {
return Robolectric.directlyOnFullStack(realActivity)
.getSupportLoaderManager();
}
}
bound with
Robolectric.bindShadowClass(ShadowFragmentActivity.class);
This appears to work for the moment. I will have to try further to see if it does what I want. No idea if this is in any way the correct way to go or not.

Robolectric 1.x had poor support for the Android support library. Robolectric 2.0 is significantly better, and has full support for fragments. An alpha was just released today:
<dependency>
<groupId>org.robolectric</groupId>
<artifactId>robolectric</artifactId>
<version>2.0-alpha-1</version>
<dependency>

Related

After migrating to AndroidX: Cannot instantiate class: androidx.appcompat.widget.ShareActionProvider

I migrated to AndroidX (using the wizard in Android Studio), and I'm having problems with the share action provider. The wizard changed (among many other things) app:actionProviderClass="android.support.v7.widget.ShareActionProvider" to app:actionProviderClass="androidx.appcompat.widget.ShareActionProvider", in my detailactivity.xml file.
The app compiles fine, and runs fine, too -- so long as I install it on my device over USB. However, if I compile a signed APK, and install that, I get the following runtime error (when starting the detailfragment):
W/SupportMenuInflater: Cannot instantiate class: androidx.appcompat.widget.ShareActionProvider
I didn't notice this problem while developing, since I run/test the app on my device through USB. However, when I'm now testing the (signed/minified) APK, the SHARE button does not work. How can I troubleshoot and fix this? For instance, why does it fail on the signed/minified APK fail, while it works fine when installing on same device through USB?
It's hard to tell specifically where (in the code) the warning occurs, since the code in the APK is minified. Perhaps I could create an APK where the code is not minified, so I'd get proper references to lines in the source code (in Android Studio LogCat)?
For reference, here is an excerpt from the class where the warning occurs. I'm assuming the warning occurs somewhere here, as this is what's referencing the shareActionProvider?
import androidx.appcompat.widget.ShareActionProvider;
public class ScreenSlidePageFragment extends Fragment implements LoaderManager.LoaderCallbacks<Cursor> {
private ShareActionProvider mShareActionProvider;
public ScreenSlidePageFragment() {
setHasOptionsMenu(true); // only the share button
}
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.detailfragment, menu);
MenuItem menuItem = menu.findItem(R.id.action_share);
mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menuItem);
if (mShareActionProvider != null && mImageData != null) {
mShareActionProvider.setShareIntent(createShareImageIntent());
}
}
}
I got some help on #android-dev (IRC), and it seems the problem was that the minifier (for some reason) removes the androidx.appcompat.widget.ShareActionProvider class. Turning off minification produced a working APK.
The fix was to update my proguard-rules.pro file with a new "keep" line, to prevent it from removing the class. From before, I had a similar rule for the old Android Support Library, so I added the second line below and now it works.
-keep class android.support.v7.widget.** { *; }
-keep class androidx.appcompat.widget.** { *; }
thanks #melatonina!

Using actionbarsherlock with android-support-v4 (Version 23)

I'm developing an app with actionbarsherlock and the ABS project is currently using android-support-v4 library (Version 18). Now I want to extend my project to support Android 6.0 and in order to use some of the methods like
ContextCompat.checkSelfPermission(thisActivity, Manifest.permission.WRITE_CALENDAR)
or
ActivityCompat.shouldShowRequestPermissionRationale(thisActivity, Manifest.permission.READ_CONTACTS)
I require support library version 23. But ABS project is not compatible with this latest library and gives error like mAdded cannot be resolved or is not a field in Watson.java
Also, please don't suggest me to use AppCompatActivity instead of ABS as I tried it but getting stuck in a web of other mess as my project is quite big.
I'm facing the same problem. Here's my solution:
Clone ActionBarSherlock
No instance field mFragments of type Landroid/support/v4/app/FragmentManagerImpl;
// android.support.v4.app.FragmentActivity
// com.android.support:support-v4:22.+
final FragmentManagerImpl mFragments = new FragmentManagerImpl();
// com.android.support:support-v4:23.+
final FragmentController mFragments = FragmentController.createController(new HostCallbacks());
// android.support.v4.app.FragmentManager.FragmentManagerImpl
ArrayList<Fragment> mAdded;
So we need to get instance of FragmentManagerImpl to access mAdded field
// android.support.v4.app.FragmentActivity
public FragmentManager getSupportFragmentManager() {
return mFragments.getSupportFragmentManager();
}
// android.support.v4.app.FragmentController
public FragmentManager getSupportFragmentManager() {
return mHost.getFragmentManagerImpl();
}
Add the following method to the android.support.v4.app.Watson class
#Nullable
private List<Fragment> getAddedFragments() {
return ((FragmentManagerImpl) getSupportFragmentManager()).mAdded;
}
Add the following code to onCreatePanelMenu, onPreparePanel and onMenuItemSelected methods and replace mFragments.mAdded with fragments
List<Fragment> fragments = getAddedFragments();
FloatMath
Historically these methods were faster than the equivalent double-based
{java.lang.Math} methods. On versions of Android with a JIT they
became slower and have since been re-implemented to wrap calls to
{java.lang.Math}. {java.lang.Math} should be used in
preference.
All methods were removed from the public API in version 23.
#deprecated Use {java.lang.Math} instead.
Replace all of occurrences of FloatMath with Math in com.actionbarsherlock.internal.nineoldandroids.view.animation.AnimatorProxy

Robolectric is-singlepane(/single-fragment)-test

I just started to try Robolectric and as I am currently playing around with the Dynamic Single/Dualpane-Fragment example from the google developer page I thought integrating it there and doing some basic tests.
The first thing I wanted to test is does the single/dualpane handling work correctly.
So its basically down to:
small device & portrait -> single-pane
large device & langscape -> dual-pane
As the code for the example is online (and as it is a standard template in Android-Studio) I am not gonna copy it here again. Just one thing: On launch the activity determines if its single- or dual pane by checking:
if (findViewById(R.id.exercise_detail_container) != null) [..]
It seems that for Robolectric its always dualpane.
So my singlepane test is pretty straight forward:
#Config(emulateSdk = 18)
#RunWith(RobolectricTestRunner.class)
public class SinglePaneTest {
private ExerciseListActivity activity;
private FragmentManager fragmentManager;
#Before
#Config(qualifiers = "port-small")
public void setup() {
this.activity = Robolectric.buildActivity(ExerciseListActivity.class).create().resume().get();
this.fragmentManager = activity.getFragmentManager();
}
#Test
#Config(qualifiers = "port-small")
public void testSinglePane() {
assertNull(activity.findViewById(R.id.exercise_detail_container));
}
}
But the test fails.
Can somebody tell me why? This should be working perfectly fine, shouldn't it?
Just for the record: Yep, in the emulator everything is working fine.
Try reversing order: small-port Don't know if will fix, but it is true that they must appear in the order in the table: http://developer.android.com/guide/topics/resources/providing-resources.html
if you use multiple qualifiers for a resource directory, you must add them to the directory name in the order they are listed in the table.
Robolectric example supports this ordering:
Qualifiers for the resource resolution, such as "fr-normal-port-hdpi".
This seems to be fixed with version 2.4 of robolectric!

how to use fest for android

I am trying to use FEST with an android app testing
I want to check if some views in the activity got created and initialized correctly
#Test
public void testSplashScreenActivity() throws Exception{
SplashScreenActivity activity = Robolectric.buildActivity(SplashScreenActivity.class).create().start().resume().get();
//here i want to write fest assertions
}
however, when i assertThat(activity.welcomeText)..... nothing is allowed after this line?
i wrote import static org.fest.assertions.api.ANDROID.assertThat;
in my imports, yet the api doesnt seem to be available
what to do to make my test project give me the api i need ?

Use external application fragment/activity inside application

Is it possible to use a fragment/activity from an external application and use as it is embedded?
For example: embed a PDF reader fragment from a PDF reader application.
May be a little bit late, but still feel that it can be added and might help others.
For activity, there's really no point to have it embedded, there's convenient way to use other apps activities - start it with intent. For fragments at might make sense in case of implementation some kind of 'plug-in' functionality inside the app.
There's an official way to use code from other applications (or load code from network) in Android Blog 'Custom Class Loading in Dalvik'. Please note, the android is not much different from other platforms/environments, so both parts (your app and fragment You want load into your app) should support some kind of contract. That means You cannot load any component from any application, which is quite common and there are number of reasons for it to be that way.
So, here's some small example of the implementation. It consists of 3 parts:
Interfaces project - this project contains definitions of interfaces which should be loaded by main app in order to use external classes:
package com.example.test_interfaces;
import android.app.Fragment;
/**
* Interface of Fragment holder to be obtained from external application
*/
public interface FragmentHolder {
Fragment getFragment();
}
For this example we need only single interface just to demonstrate how to load the fragment.
Plug-in application, which contains the code You need to load - in our case it's a fragment. Please note, that this project in your IDE should depend on Interface one using 'provided' type and without exporting, because it will be imported by main application.
Fragment, we're going to load PlugInFragment:
package com.sandrstar.plugin;
import com.example.test_interfaces.FragmentHolder;
public class PlugInFragment extends Fragment implements FragmentHolder {
#Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
// Note that loading of resources is not the same as usual, because it loaded actually from another apk
final XmlResourceParser parser = container.getContext().getPackageManager().getXml("com.sandrstar.plugin", R.layout.fragment_layout, null);
return inflater.inflate(parser, container, false);
}
#Override
public Fragment getFragment() {
return this;
}
}
And it's layout fragment_layout.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#android:color/black">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is from fragment"
android:textColor="#android:color/white"/>
</LinearLayout>
Main application which wants to load the fragment from another application. It should have Interface project imported:
Activity itself MyActivity:
public class MyActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
try {
Class<?> requiredClass = null;
final String apkPath = getPackageManager().getApplicationInfo("com.sandrstar.plugin",0).sourceDir;
final File dexTemp = getDir("temp_folder", 0);
final String fullName = "com.sandrstar.plugin.PlugInFragment";
boolean isLoaded = true;
// Check if class loaded
try {
requiredClass = Class.forName(fullName);
} catch(ClassNotFoundException e) {
isLoaded = false;
}
if (!isLoaded) {
final DexClassLoader classLoader = new DexClassLoader(apkPath,
dexTemp.getAbsolutePath(),
null,
getApplicationContext().getClassLoader());
requiredClass = classLoader.loadClass(fullName);
}
if (null != requiredClass) {
// Try to cast to required interface to ensure that it's can be cast
final FragmentHolder holder = FragmentHolder.class.cast(requiredClass.newInstance());
if (null != holder) {
final Fragment fragment = holder.getFragment();
if (null != fragment) {
final FragmentTransaction trans = getFragmentManager().beginTransaction();
trans.add(R.id.fragmentPlace, fragment, "MyFragment").commit();
}
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
And it's layout main.xml:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipChildren="false"
android:id="#+id/root">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="#drawable/down_image" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/fragmentPlace"
android:layout_centerInParent="true" />
</RelativeLayout>
And the finally we able to observe the following on the real device:
Possible issues handling (thanks to #MikeMiller for the update):
If you get the following error in the call to classLoader.loadClass:
java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
Make sure the fragment modules are included in the main app (as 'compiled')
If you get a NameNotFoundException in the call to context.getPackageManager().getApplicationInfo(packageName,0).sourceDir, then make sure the fragment is in an installed APPLICATION (not just a library dependency). Follow the steps below to make sure that's the case:
1) In the main application's build.gradle, change apply plugin: 'android-library' to apply plugin: 'android' and make sure there's a dummy activity java file. In the main application, remove the dependency on the fragment module (It's not specified in step 3, but I had to add a dependency on the fragment module to the main application. But the fragment module is now an activity application, and you can't have dependencies on those) or you'll get this: Error:Dependency unspecified on project resolves to an APK archive which is not supported as a compilation dependency.
2) Run the fragment module (which you can do now, because it's an activity application). That installs it in a way that the getApplicationInfo call can find it Revert build.gradle and add the dependency back in the main app (as a 'compile' dependency) Everything should work now. When you make updates to the fragment code, you won't need to go through this process again. You will, though, if you want to run on a new device or if you add a new fragment module. I hope this is able to save someone the time I spent trying to resolve the above errors.
Android L
Seems, based on normal multidex support with Android L, above steps are not needed, because class loading is different. Approach, described in multidex support can be used instead of Android Blog 'Custom Class Loading in Dalvik', because it clearly states that:
Note: The guidance provided in this document supersedes the guidance
given in the Android Developers blog post Custom Class Loading in
Dalvik.
Probably, changes in android.support.multidex might be needed to reuse that approach.
No, you can not "reuse" code from other applications. The only official way is to use Intent to invoke the whole Activity.
I use a quite similar approach to sandrstart. Maybe it's less secure.
I use a normal Classloader derived from a packagecontext created with the plugin package name. The package names of all plugins are loaded and saved along with other configurations from a config website.
Context ctx = createPackageContext(packetName, Context.CONTEXT_INCLUDE_CODE |
Context.CONTEXT_IGNORE_SECURITY);
ClassLoader cl = ctx.getClassLoader();
Class<?> c = cl.loadClass(className);
Fragment fragObj = (Fragment)c.newInstance();
But I wanted to stress that my approach and I think sandrstar's approach only work with the android internal android.app.Fragment class.
I was trying (again) as part for adopting to android 9 to switch from android.app.Fragment (depricated) to android.support.v4.Fragment but could'nt get it to work.
The reason is that:
apk1:framework.class.A == apk2.framework.class.A but
apk1:someJarOrAar.class.B != aps2.someJarOrAar.class.B
even if the same exact jar/aar is used in both projects.
So I will always get a ClassCastException on (Fragment)c.newInstance();.
I have tried to cast via the classloader from the plugin and cast via classloader from main package but to no avail. I think there is no way around it as it can't be guaranteed that jars/aars are really the same even if they have same names and same classnames in them so it's a security issue to treat them as different even if they are the same.
I surely hope for some workaround (other than keep on using the android.app.Fragment even under android 9 as I will do for now) but I will also be grateful for comments for why it is definetly not possible with shared classes (such as support.v4.Fragment).

Categories

Resources