Starting Android Pie (API 28), Google isn't allowing using a single WebView instance in 2 different processes.
Documentation: https://developer.android.com/reference/android/webkit/WebView.html#setDataDirectorySuffix(java.lang.String)
As required, I called WebView.setDataDirectorySuffix("dir_name_no_separator") but unfortunately, I get an exception.
I tried to call this method inside the 2nd process Service onCreate().
java.lang.RuntimeException: Unable to create service com.myapp.service.MyService: java.lang.IllegalStateException: Can't set data directory suffix: WebView already initialized
at android.app.ActivityThread.handleCreateService(ActivityThread.java:3544)
at android.app.ActivityThread.access$1300(ActivityThread.java:199)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1666)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
Caused by: java.lang.IllegalStateException: Can't set data directory suffix: WebView already initialized
at android.webkit.WebViewFactory.setDataDirectorySuffix(WebViewFactory.java:136)
at android.webkit.WebView.setDataDirectorySuffix(WebView.java:2165)
at com.myapp.service.MyService.onCreate(MyService.java:134)
I couldn't find any reason for that exception. I didn't call this method twice nor I called it in my main process. Any ideas?
Solved.
My project hosts AdMob ads and I call the MobileAds.initialize() method inside my Application class onCreate(). The ads initializer loads a WebView which is now forbidden to do in a new process before you call the WebView.setDataDirectorySuffix("dir_name_no_separator") method.
When the second process is created, it also goes through the same application create flow, meaning it calls the same onCreate() inside the Application class, which calls the MobileAds.initialize() that tries to create a new WebView instance and by that causes the crash.
IllegalStateException: Can't set data directory suffix: WebView already initialized
How I solved this?
I get the process name using the below method and check if it's my main process - call the MobileAds.initialize() method and if it's my second process, call the
WebView.setDataDirectorySuffix("dir_name_no_separator") method.
Get process name:
public static String getProcessName(Context context) {
ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == android.os.Process.myPid()) {
return processInfo.processName;
}
}
return null;
}
Application class onCreate():
if (!Utils.getProcessName(this).equals("YOUR_SECOND_PROCESS_NAME")) {
MobileAds.initialize(this);
} else {
WebView.setDataDirectorySuffix("dir_name_no_separator")
}
To summarize the fix with all the improvements, this is the code in Kotlin:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
if (packageName != Application.getProcessName()) {
WebView.setDataDirectorySuffix(Application.getProcessName())
}
}
Add it to your Application class to onCreate() method.
Note this is will only fix problem with maximum 2 processes. If your app is using more, you have to provide different WebView suffix for each of them.
when error due to ads, then in application class
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val process = getProcessName()
if (packageName != process) WebView.setDataDirectorySuffix(process)
}
MobileAds.initialize(this)
AudienceNetworkAds.initialize(this)
} catch (e: Error) {
Timber.e(e)
} catch (e: Exception) {
Timber.e(e)
}
Related
I'm making a plugin for Flutter to handle fcm messages using an android native library.
As we know when a message is received by FCM, it starts the app (It's application class) and runs the codes within Application#onCreate block, so we can run native code when app starts by fcm in the background.
My question is, is it possible to run flutter code at that time when application starts?
For instance if the message was received:
Application class:
public class Application extends FlutterApplication {
#Override
public void onCreate() {
super.onCreate();
// Start flutter engine
// Invoke a dart code in the Plugin using methodChannel or etc.
}
}
Short answer, Yes
You can call a Dart method in background using it's handle key.
1. Register your plugin in the background
Implement a custom application class (override FlutterApplication)
public class MyApp extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {
#Override
public void registerWith(io.flutter.plugin.common.PluginRegistry registry) {
// For apps using FlutterEmbedding v1
GeneratedPluginRegistrant.registerWith(registry);
// App with V2 will initialize plugins automatically, you might need to register your own however
}
}
Remember to register the class in the AndroidManifest by adding android:name=".MyApp" to <application> attributes.
What is embedding v2?
2. Create a setup function as top level function in your flutter code
/// Define this TopLevel or static
void _setup() async {
MethodChannel backgroundChannel = const MethodChannel('flutter_background');
// Setup Flutter state needed for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
// This is where the magic happens and we handle background events from the
// native portion of the plugin.
backgroundChannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'handleBackgroundMessage') {
final CallbackHandle handle =
CallbackHandle.fromRawHandle(call.arguments['handle']);
final Function handlerFunction =
PluginUtilities.getCallbackFromHandle(handle);
try {
var dataArg = call.arguments['message'];
if (dataArg == null) {
print('Data received from callback is null');
return;
}
await handlerFunction(dataArg);
} catch (e) {
print('Unable to handle incoming background message.\n$e');
}
}
return Future.value();
});
3. Create a top level callback that will get the background message and calls it
_bgFunction(dynamic message) {
// Message received in background
// Remember, this will be a different isolate. So, no widgets
}
4. Get the handle key of the background function and setup and send it to native via MethodChannel
// dart:ui needed
CallbackHandle setup PluginUtilities.getCallbackHandle(_setup);
CallbackHandle handle PluginUtilities.getCallbackHandle(_bgFunction);
_channel.invokeMethod<bool>(
'handleFunction',
<String, dynamic>{
'handle': handle.toRawHandle(),
'setup': setup.toRawHandle()
},
);
5. Save them into SharedPref in the native side
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
String methodName = call.method
if (methodName == "handleFunction") {
long handle = call.argument("handle");
long setup = call.argument("setup");
// save them
}
}
6. When background is awaken, start a background isolate
FlutterMain.ensureInitializationComplete(context, null)
val appBundlePath = FlutterMain.findAppBundlePath()
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(setupHandleYouHadSaved)
FlutterNativeView backgroundFlutterView = FlutterNativeView(context, true)
val args = FlutterRunArguments()
args.bundlePath = appBundlePath
args.entrypoint = flutterCallback.callbackName
args.libraryPath = flutterCallback.callbackLibraryPath
backgroundFlutterView?.runFromBundle(args)
// Initialize your registrant in the app class
pluginRegistrantCallback?.registerWith(backgroundFlutterView?.pluginRegistry)
7. When your plugin is registered, create a background channel and pass it to
val backgroundChannel = MethodChannel(messenger, "pushe_flutter_background")
8. Call the setup method that would call and give the message to you callback
private fun sendBackgroundMessageToExecute(context: Context, message: String) {
if (backgroundChannel == null) {
return
}
val args: MutableMap<String, Any?> = HashMap()
if (backgroundMessageHandle == null) {
backgroundMessageHandle = getMessageHandle(context)
}
args["handle"] = backgroundMessageHandle
args["message"] = message
// The created background channel at step 7
backgroundChannel?.invokeMethod("handleBackgroundMessage", args, null)
}
The sendBackgroundMessageToExecute will execute the dart _setup function and pass the message and callback handle. In the step 2, callback will be called.
Note: There are still certain corner cases you may want to consider (for instance thread waiting and ...). Checkout the samples and see the source code.
There are several projects which support background execution when app is started in the background.
FirebaseMessaging
Pushe
WorkManager
I did it a different, simpler way compared to Mahdi's answer. I avoided defining an additional entrypoint/ callback, using PluginUtilities, callback handles, saving handles in SharedPreferences, passing messages with handles between dart and platform, or implementing a FlutterApplication.
I was working on a flutter plugin (so you don't have to worry about this if you use my library for push notifications 😂), so I implement FlutterPlugin. If I want to do background processing and the Flutter app isn't running, I just launch the Flutter app without an Activity or View. This is only necessary on Android, since the FlutterEngine/ main dart function runs already runs when a background message is received in an iOS app. The benefit is that this is the same behaviour as iOS: a Flutter app is always running when the app is launched, even if there is no app shown to the user.
I launch the application by using:
flutterEngine = new FlutterEngine(context, null);
DartExecutor executor = flutterEngine.getDartExecutor();
backgroundMethodChannel = new MethodChannel(executor, "com.example.package.background");
backgroundMethodChannel.setMethodCallHandler(this);
// Get and launch the users app isolate manually:
executor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());
I did this to implement background push notification handling in a library, ably_flutter. It seems to work well. The FlutterEngine/ application is launched only when the application is not already running. I do this by keeping track of the activity (using ActivityAware):
if (isApplicationInForeground) {
// Send message to Dart side app already running
Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else if (AblyFlutterPlugin.isActivityRunning) {
// Flutter is already running, just send a background message to it.
Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else {
// No existing Flutter Activity is running, create a FlutterEngine and pass it the RemoteMessage
new PushBackgroundIsolateRunner(context, asyncCompletionHandlerPendingResult, message);
}
Then, I just use a separate MethodChannel to pass the messages back to the dart side. There's more to this parallel processing (like telling the Java side that the App is running/ ready. Search for call.method.equals(pushSetOnBackgroundMessage) in the codebase.). You can see more about the implementation PushBackgroundIsolateRunner.java at ably_flutter. I also used goAsync inside the broadcast receiver to extend the execution time from 10s to 30s, to be consistent with iOS 30s wall clock time.
You can use a headless Runner to run dart code from an Application class (or service, broadcast receiver etc).
There's a good in depth article on how to implement this: https://medium.com/flutter/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124
According to my knowledge we have to call a class GeneratedPluginRegistrant.registerWith(this); at oncreate method where flutter code has to run.
If you mean you want to run some arbitrary Dart code in the background you can use the this plugin we created which really facilitates the use of background work.
You can register a background job that should be executed at a given point in time and it will call back in to your Dart code where you can run some code in the background.
//Provide a top level function or static function.
//This function will be called by Android and will return the value you provided when you registered the task.
//See below
void callbackDispatcher() {
Workmanager.defaultCallbackDispatcher((echoValue) {
print("Native echoed: $echoValue");
return Future.value(true);
});
}
Workmanager.initialize(callbackDispatcher)
Then you can schedule them.
Workmanager.registerOneOffTask(
"1",
"simpleTask"
);
The String simpleTask will be returned in the callbackDispatcher function once it starts running in the background.
This allows for you to schedule multiple background jobs and identify them by this id.
I am developer of a password manager app which provides an Android Autofill service (Android 8+).
Some users requested that it should be possible to disable this service on a per-target-app basis. In the autofill service's onFillRequest I am adding a "dataset" with "Disable autofill for [package name]" like this:
var sender = IntentBuilder.GetDisableIntentSenderForResponse(this, query, isManual, isForDisable);
RemoteViews presentation = AutofillHelper.NewRemoteViews(PackageName,
GetString(Resource.String.autofill_disable , new Java.Lang.Object[] { query}), Resource.Drawable.ic_menu_close_grey);
var datasetBuilder = new Dataset.Builder(presentation);
datasetBuilder.SetAuthentication(sender);
foreach (var autofillId in autofillIds)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
}
responseBuilder.AddDataset(datasetBuilder.Build());
When the user clicks the "Disable dataset", an activity is launched which stores the package for which Autofill should be disabled and then immediately finishes itself.
My question: what should I return as a reply from that activity to indicate that Autofill should be invisible from now on?
I am currently doing
bool isManual = Intent.GetBooleanExtra(ChooseForAutofillActivityBase.ExtraIsManualRequest, false);
Intent reply = new Intent();
FillResponse.Builder builder = new FillResponse.Builder();
AssistStructure structure = (AssistStructure)Intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure);
StructureParser parser = new StructureParser(this, structure);
try
{
parser.ParseForFill(isManual);
}
catch (Java.Lang.SecurityException e)
{
Log.Warn(CommonUtil.Tag, "Security exception handling request");
SetResult(Result.Canceled);
return;
}
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
var autofillIds = autofillFields.GetAutofillIds();
builder.SetIgnoredIds(autofillIds);
Bundle state = new Bundle();
state.PutStringArray("AutoFillDisabledQueries", disabledValues.ToArray());
builder.SetClientState(state);
try
{
var response = builder.Build();
reply.PutExtra(AutofillManager.ExtraAuthenticationResult, response);
}
catch (Exception e)
{
Kp2aLog.LogUnexpectedError(e);
throw;
}
SetResult(Result.Ok, reply);
But
1.) The prompt for autofill does not disappear
2.) if I click disable again, the target app is force-closed (see end of message for details)
so that's obviously not the way to go... Any ideas? Thanks a lot!
Regarding point 2 above, I see the following in logcat:
12-17 09:48:31.865 Google Pixel Error 11711 AndroidRuntime java.lang.RuntimeException: Failure delivering result ResultInfo{who=#android:autoFillAuth:, request=16121857, result=-1, data=Intent { (has extras) }} to activity {com.sonelli.juicessh/com.sonelli.juicessh.activities.ManageConnectionActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'java.lang.Object java.util.List.get(int)' on a null object reference
at android.app.ActivityThread.deliverResults(ActivityThread.java:4361)
at android.app.ActivityThread.handleSendResult(ActivityThread.java:4403)
at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:49)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1809)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6680)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
setIgnoredIds() should be used for fields that are not autofillable (for example, if the screen had an username, password, and captcha fields, you could use to ignore the latter).
If you want to disable autofill for the activity, you need to return a Fillresponse with just the [disableAutofill()][1] set on it. Notice that disabling it doesn't persist after reboots, so you should keep track of it internally, and call that API if the activity triggers autofill again.
Is there a way to launch the Square POS Android app from Xamarin.Forms? I noticed all the SDK examples are in Java. Does the app have to be written using Java?
EDIT: Update
I created Bindings library in Visual Studio using the square android SDK aar file. Now, it launches the Point-of-Sale app but then it afterwords no result data is being sent back to the application. Instead, it goes to an error screen where it says "Webpage not available"
The webpage at
intent#intent;action=com.squareup.pos.action.CHARGE (more..)
could not be loaded because
net: ERROR_UNKNOWN_URL_SCHEME
I am trying to do this in a WebView by overriding ShouldOverrideUrlLoading
public override bool ShouldOverrideUrlLoading(WebView view, string url)
{
if (url.StartsWith("http:") || url.StartsWith("https:"))
{
view.LoadUrl(url);
return false;
}
if (url.Contains("CHARGE"))
{
// Process Square Point-Of-Sale Transaction
ChargeRequest chargeRequest =
new ChargeRequest.Builder(1, CurrencyCode.Usd)
.Note("Test")
.RequestMetadata("WEB_CALLBACK_URI=mycallbackurl")
.RestrictTendersTo(ChargeRequest.TenderType.Card, ChargeRequest.TenderType.CardOnFile, ChargeRequest.TenderType.Cash)
.CustomerId("MyCustomerID")
.Build();
try
{
if (PosClient == null)
{
new AlertDialog.Builder(this.activity).SetMessage("Square Point-Of-Sale Client Error").Show();
}
Intent chargeIntent = PosClient.CreateChargeIntent(chargeRequest);
this.activity.StartActivityForResult(chargeIntent, CHARGE_REQUEST_CODE);
}
catch (ActivityNotFoundException e)
{
new AlertDialog.Builder(this.activity).SetMessage(e.Message).Show();
}
}
return false;
}
EDIT: Update
I added an override for OnRecieveError
The errorCode is
Android.Webkit.ClientError.Unsupported Scheme
In the Webview Object there's this
Java.Lang.NoSuchMethodError: no non-static method
"Landroid/webkit/WebView;.getRendererPriorityWaivedWhenNotVisible()Z"
Java.Lang.NoSuchMethodError: no non-static method
"Landroid/webkit/WebView;.getRendererRequestedPriority()I"
EDIT: Update
I have it sort of working now. In the ShouldOverrideUrlLoading method, I changed it to return true if it was processing a transaction. I also had to override OnActivityResult and parse the data coming back from the Android app. It now launches the Square POS App and Returns data back to the calling app. Now, I just have figure out how to process the transaction from there.
I am using cast feature in my application. It was working fine but suddenly I can see the increase in the number of crashes on play store console.
I am initializing CastContext properly as defined in the guidelines and Moreover, I am checking that device is compatible or not before calling this method CastContext.getSharedInstance(context) So that should not be an issue.
I am not able to reproduce this crash even on emulators with or without google-play-services one.
Any help will be appreciated.
Crash :
Fatal Exception: java.lang.RuntimeException: Unable to start activity
ComponentInfo{... .activity.TVActivityPhone}:
java.lang.RuntimeException: com.google.android.gms.dynamite.DynamiteModule$zza: Remote load
failed. No local fallback found. at
android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2677)
at
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2747)
at android.app.ActivityThread.access$900(ActivityThread.java:187) at
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1584)
at android.os.Handler.dispatchMessage(Handler.java:111) at
android.os.Looper.loop(Looper.java:194) at
android.app.ActivityThread.main(ActivityThread.java:5877) at
java.lang.reflect.Method.invoke(Method.java) at
java.lang.reflect.Method.invoke(Method.java:372) at
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1020)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:815) Caused
by java.lang.RuntimeException:
com.google.android.gms.dynamite.DynamiteModule$zza: Remote load
failed. No local fallback found.
Code I am getting an error inside the if the condition that means, it's not about the google play service availability.
if (googlePlayServicesVerified(context)) { // checking (result==ConnectionResult.SUCCES)
Log.d("TAG", "instantiated");
castContext = CastContext.getSharedInstance(context);
} else {
Log.e(TAG, "FAILED");
}
Filed bug to google:
https://issuetracker.google.com/issues/65359941
** Update **
Check these two issues :
https://issuetracker.google.com/issues/65359941
https://issuetracker.google.com/issues/79405933
The temporary solution is in my answer.
This is the temporary solution.
1) Your app should always check GPS version before using any Cast APIs
2) Allow CastContext.getSharedInstance() to fail. Probably throw/catch an exception (or alternatively return null).
3) Make sure, you don't break anything if the dynamite module fails to load. There are some UI widgets that are initialized implicitly which calls CastContext.getSharedInstance(), such as MiniControllerFragment. You should avoid letting it crash if dynamite fails to load.
public static boolean isAvailable(Context context)
{
GoogleApiAvailability availability = GoogleApiAvailability.getInstance();
return isGooglePlayServicesAvailable(context, availability) &&
isCastContextAvailable(context);
}
public static boolean isAvailable(Context context) {
if (googlePlayServicesVerified(context)) {
try {
castContext = CastContext.getSharedInstance(context);
Log.d(TAG, "CastContext instantiated");
} catch (Exception e) {
Log.report(e);
castContext = null;
}
} else {
CrashReporter.report("CastContext FAILED to be instantiated : googlePlayServicesVerified() has failed."));
castContext = null;
}
}
I get this issue when play services are out of date (mainly on emulators running API 24). This worked for me:
GoogleApiAvailability googleApiAvailability = GoogleApiAvailability.getInstance();
int resultCode = googleApiAvailability.isGooglePlayServicesAvailable(this);
if (resultCode == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) {
googleApiAvailability.getErrorDialog(this, resultCode, 1000).show();
return;
}
startApp();
I run this code in an Activity which checks if MainActivity should be started
I developed an application that contains a homescreen with an article list.
If you click on it, you access the detail in another screen.
I implemented the ActionBarSherlock, so I used the "up" button pattern for this activity.
Then I added a widget to this application. When you click on the widget, you access directly the detail activity.
The "up" button has been implemented following the Google recommandations (http://developer.android.com/training/implementing-navigation/ancestral.html).
My problem is that on API Level 15 and below, it works perfectly. It calls the following code :
#Override
public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) {
String action = activity.getIntent().getAction();
return action != null && !action.equals(Intent.ACTION_MAIN);
}
But on JellyBean, the code used is :
public boolean shouldUpRecreateTask(Intent targetIntent) {
try {
PackageManager pm = getPackageManager();
ComponentName cn = targetIntent.getComponent();
if (cn == null) {
cn = targetIntent.resolveActivity(pm);
}
ActivityInfo info = pm.getActivityInfo(cn, 0);
if (info.taskAffinity == null) {
return false;
}
return !ActivityManagerNative.getDefault().targetTaskAffinityMatchesActivity(mToken, info.taskAffinity);
} catch (RemoteException e) {
return false;
} catch (NameNotFoundException e) {
return false;
}
}
The first part of the method retrieves information on the activity that should be loaded if stack must be recreated.
But I still don't understand what does the line :
!ActivityManagerNative.getDefault().targetTaskAffinityMatchesActivity(mToken, info.taskAffinity);
Can anyone help me on this line, I really need to find out how to obtain true by initializing everything well ?
Its a boolean method it has to return something. If it needs to return a true boolean variable for it work, you have to do so!
From the official Documentation:
Returns true if the app should recreate the task when navigating 'up'
from this activity by using targetIntent.
If this method returns false the app can trivially call navigateUpTo(Intent)
using the same parameters to correctly perform up navigation.
If this method returns false, the app should synthesize a new task stack by using
TaskStackBuilder or another similar mechanism to perform up navigation.
The affinity indicates which task an activity prefers to belong to. By default, all the activities from the same application have an affinity for each other. So, by default, all activities in the same application prefer to be in the same task. However, you can modify the default affinity for an activity. Activities defined in different applications can share an affinity, or activities defined in the same application can be assigned different task affinities.