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).
Related
First I should explain what my ultimate goal is. I develop Android Apps, mostly using WebViews. They are great in various aspects, but one thing they don't do very well is "matching the native UI", especially fonts. Some phones (such as Samsung's) support using Flipfont to switch the default font for the entire system, but for some reasons, no browsers (as far as I know) adapt that setting and display webpages with the Flipfont settings. The same is true for WebViews, and thus creating inconsistent user experience.
But I think there should be a way to let WebViews using the Flipfont font. By studying the decompiled source code of iFont, I think I've figured out how to extract the .ttf file from the assets of a given Flipfont package, of which the package name always begins with com.monotype.android.font. And after that, I supposedly can let the WebView use that .ttf file as the default font. The problem is, I don't know which package I should extract, that is, I don't know which Flipfont package is currently in use, if any. It appears that iFont cannot determine that either; there's no place in that app that tells me explicitly "You're using font xxx".
But obviously there must be a way to determine that, because the Flipfont setting dialog shows me exactly that. However, I failed to decompile the setting dialog to study how it is done. From Logcat, it appears that the setting dialog has something to do with the package com.sec.android.easysettings and com.android.settings, but decompiling the corresponding apk's (which are under /system/app/easysettings and /system/priv-app/SecSettings, respectively) both result in no source code at all, only resources (can someone also explain why this happens?).
So does anyone know how to determine the current Flipfont package?
After more digging I finally found a solution that works.
For those system that uses Flipfont, the Typeface class has additional methods that allows one to obtain information regarding the Flipfont setting (I figured that out here). However since those methods are not defined on the standard Typeface class, one would need to use reflection to call those methods, with exception catching of course.
I came up with the following FontUtil class, of which getFlipFont method returns the File object of the current Flipfont .ttf file if there's one, or null if there's none.
import android.content.Context;
import android.graphics.Typeface;
import java.io.File;
import java.lang.reflect.Method;
public class FontUtil {
#SuppressWarnings("JavaReflectionMemberAccess")
public static File getFlipFont(Context context) {
try {
Method m = Typeface.class.getMethod("getFontPathFlipFont", Context.class, int.class);
String path = m.invoke(null, context, 1).toString();
if (!path.equals("default")) {
File file = new File(path + "/DroidSansFallback.ttf");
if (file.exists()) return file;
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
After that, my goal was achieved by the following, should anyone cares. Add the following CSS on the webpages of my app:
#font-face {
font-family: Flipfont;
src: url(flipfont.ttf);
}
body {
font-family: Flipfont;
}
Next, use something like this in my activities:
private File flipFont;
private WebView webView;
#Override
protected void onCreate(Bundle savedInstanceState) {
...
flipFont = FontUtil.getFlipFont(this);
...
webView.setWebViewClient(new AppWebViewClients());
...
}
private class AppWebViewClients extends WebViewClient {
#Nullable
#Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
if (url.endsWith("flipfont.ttf") && flipFont != null) {
try {
InputStream stream = new FileInputStream(flipFont);
return new WebResourceResponse("application/x-font-ttf", "UTF-8", stream);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
return super.shouldInterceptRequest(view, request);
}
}
Now my WebView apps uses exactly the same font as any other native apps.
Update 2021.4.25:
Today I realize that on Samsung Note 9, the method name is semGetFontPathOfCurrentFontStyle() instead, so one should also check if this method is available. Also, in some methods of changing the font, the font filename might end up being DroidSans.ttf instead of DroidSansFallback.ttf, so it is also necessary that one should check the former as a "fallback" (in the opposite order to the filenames).
I am attempting to develop a "Dynamic" Android application.
Dynamic in the sense that I have an activity listed in the manifest that is "built" at runtime.
I can build the required activity fine, however, when I attempt to start it my application fails with...
java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.research.ps/com.research.Dynamic}: java.lang.ClassNotFoundException:
Didn't find class "com.research.Dynamic" on path: DexPathList[[zip file "/data/app/com.research.ps-1/base.apk"],nativeLibraryDirectories=[/data/app/com.research.ps-1/lib/arm,
/data/app/com.research.ps-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]
Is there an approach I can take to successfully instantiate an Android Activity at runtime?
Is there a way I can add a "temporary" or "shell" activity onto my application path? and then replace the "temporary" activity with my dynamic instance?
UPDATE
My Manifest XML contains this entry
<activity
android:name=".Dynamic"
android:label="#string/title_activity_dynamic"
android:theme="#style/AppTheme.NoActionBar" />
However, there is no Activity called "Dynamic" contained within my application.
I am using ByteBuddy to build my dynamic activity:-
final Class<? extends android.support.v7.app.AppCompatActivity> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
.subclass(android.support.v7.app.AppCompatActivity.class, IMITATE_SUPER_CLASS)
.name("com.research.Dynamic")
.make()
.load(getClass().getClassLoader(), new AndroidClassLoadingStrategy.Wrapping(this.getDir("dexgen", Context.MODE_PRIVATE)))
.getLoaded();
final Intent intent = new Intent(this, dynamicType);
startActivity(intent);
Yes you CAN start such an Activity (assuming you have a dummy manifest Activity entry). If you don't like this technique, use Fragments (they don't need entries in the manifest). Alternatively use WebView and JavaScript like Apache Cordova et-al (cross platform too !).
ByteBuddy (kudos too #Rafael Winterhalter author of Byte Buddy) looks cool, maybe a learning curve involved. Why not download the linked project and try both techniques.
Here's how to include ByteBuddy in your Android Studio Gradle project (build.gradle):
android {
compileSdkVersion 25
buildToolsVersion '25'
dependencies {
compile 'com.android.support:appcompat-v7:25'
compile 'net.bytebuddy:byte-buddy:1.7.9'
compile 'net.bytebuddy:byte-buddy-android:1.7.9'
}
}
how I can "find" my dynamically instantiate class at runtime?
External loading of DEX files (class byte code)
See my answer here and follow the links for source code and tutorial (Apache Ant {Eclipse compatible, build.xml} and Android Studio Gradle examples build.gradle of the same code, you need some custom build steps which these project's provide).
Code snippet:
// Internal storage where the DexClassLoader writes the optimized dex file to.
final File optimizedDexOutputPath = getDir(SECONDARY_DEX_INTERNAL_DIR, Context.MODE_PRIVATE);
// Initialize the class loader with the secondary dex file.
DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
optimizedDexOutputPath.getAbsolutePath(),
null,
getClassLoader());
Class libProviderClazz = null;//variable libProviderClazz of type Class
try {
// Load the library class from the class loader.
libProviderClazz = cl.loadClass(PROVIDER_CLASS);
// Cast the return object to the library interface so that the
// caller can directly invoke methods in the interface.
// Alternatively, the caller can invoke methods through reflection,
// which is more verbose and slow.
LibraryInterface lib = (LibraryInterface) libProviderClazz.newInstance();
}
catch (Exception exception)
{
// Handle exception gracefully here.
exception.printStackTrace();
}
Q: How do I add an Activity, I cannot add it to the manifest ? A: Use
Fragments, they don't need entries in the manifest.
I have managed to call a dynamically instantiated Activity and set the required layout content using ByteBuddy.
Heres how
final DynamicType.Unloaded<? extends AppCompatActivity> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
.subclass(AppCompatActivity.class)
.name(CLASS_NAME)
.method(named("onCreate").and(takesArguments(1)))
.intercept(MethodDelegation.to(TargetActivity.class).andThen(SuperMethodCall.INSTANCE))
.make();
final Class<? extends AppCompatActivity> dynamicTypeClass = dynamicType.load(getClassLoader(), new AndroidClassLoadingStrategy.Injecting(this.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();
final Intent intent = new Intent(this, dynamicTypeClass);
startActivity(intent);
The method delegation class
public class TargetActivity {
public static void intercept(Bundle savedInstanceState, #This AppCompatActivity thiz) {
thiz.setContentView(R.layout.activity_fourth);
}
}
Even though this gives the desired result it still has issues as the super.onCreate(savedInstanceState) call is made after I have called setContent (I think).
Using the excellent ByteBuddy library is a much better approach IMHO than working with DEX manipulation.
I am trying to render a checkbox in a Xamarin Forms app. There is nothing rendered at runtime, as far as I can tell the renderer is not even getting called.
Does anyone understand what I am missing or doing incorrectly?
Here is my class in Forms:
public class LegalCheckbox : View
{
public LegalCheckbox ()
{
}
}
And my custom renderer class in Droid:
public class CheckBoxRenderer : ViewRenderer<LegalCheckbox, CheckBox>
{
protected override void OnElementChanged (ElementChangedEventArgs<LegalCheckbox> e)
{
base.OnElementChanged (e);
CheckBox control = new Android.Widget.CheckBox(this.Context);
control.Checked = false;
control.Text = "I agree to terms";
control.SetTextColor (Android.Graphics.Color.Rgb (60, 60, 60));
this.SetNativeControl(control);
}
}
Along with the Assembly Directive:
[assembly: ExportRenderer(typeof(demo.LegalCheckbox), typeof(demo.Droid.CheckBoxRenderer))]
Took your code and fired up a new project with it. The code appears to function fine.
Only thin I can think that might be causing you an issue is the location of you assembly attribute. I typically place them just above the namespace declaration in the same file as my renderer.
I threw what I created up on my github maybe you can spot the difference.
https://github.com/DavidStrickland0/Xamarin-Forms-Samples/tree/master/RendererDemo
#Thibault D.
Xlabs isn't a bad project but its basically just all the code the opensource community came up with during the first year or so of Xamarin.Forms life. Its not really "Their Labs projects" and considering how much of it is marked up with Alpha Beta and the number of bugs in their issues page it's probably best not to imply that the Xamarin company has anything to do with it.
I am not sure if that is the issue but it would make more sense to me if your LegalCheckbox would inherit from a InputView rather than View.
Also, even if Xamarin.Forms does not have a Checkbox control you can still have a look at their "Labs" project here:
https://github.com/XLabs/Xamarin-Forms-Labs/wiki/Checkbox-Control
(And I can actually see that they inherit from View...)
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!
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>