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.
Related
I decided to experiment with MAUI. I am approaching first an Android App, and using Shell for navigation.
My App has 2 ways of opening:
When it's opened by the user tapping on the icon
Through a deep link, triggered by another app.
The issue I'm having is that when the app is triggered through the Deep link, I need to navigate to a specific page. I am trying to do it on the OnNewIntent accessing the Current instance of Shell, but when doing GoToAsync("my_route") it gives an error when trying to navigate to the new page.
This is what I have on my MainActivity:
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
var action = intent.Action;
var data = intent.DataString;
if (!string.IsNullOrWhiteSpace(data) && data.Contains("/data/")) {
if(Shell.Current != null)
{
Shell.Current.GoToAsync("myroute)";
// Also tried:
// - Shell.Current.GoToAsync("myroute").Wait();
// - App.Current.Dispatcher.Dispatch(async () => await Shell.Current.GoToAsync("//myroute")); (suggested by #toolmakersteve )
}
}
}
And this is the error:
Java.Lang.IllegalArgumentException: 'No view found for id 0x1
(unknown) for fragment ShellItemRenderer{19d353d}
(6c8560ab-dd58-4cbf-9e8b-2b9e12315f45 id=0x1)'
I'm assuming this has something to do with the fact that what I'm doing is not possible, so I need to find the RIGHT way to navigate to a specific page from OnNewIntent on MAUI, using Shell navigation.
UPDATE: It's also important to note that when the Deep Link triggers the app to open, there are two different behaviours:
If the app was already running, it throws the above mentioned exception
If the app was not already running, it opens regularly on the main screen, with no errors, but I would expect it to navigate to the desired Page.
Thanks!
First, make sure that GoToAsync("myroute") works if you use it somewhere more typical, such as a button press.
Assuming that works, then perhaps the intent code isn't running in the Dispatcher context (previously known as MainThread). Try:
Dispatcher.Dispatch(() => {
Shell.Current.GoToAsync("myroute");
});
VERSION 2
Perhaps deep link logic runs BEFORE App's OnResume.
If so, this might work:
In App.xaml.cs:
public partial class App : Application
{
...
public static bool FromDeepLink;
protected override void OnResume()
{
base.OnResume();
if (FromDeepLink)
{
FromDeepLink = false;
MainPage = new MainPage();
Dispatcher.Dispatch(() =>
{
Shell.Current.GoToAsync("myroute");
});
}
}
}
Then in OnNewIntent:
if (!string.IsNullOrWhiteSpace(data) && data.Contains("/data/")) {
App.FromDeepLink = true;
}
Conceptually #ToolmakerSteve answer is correct, but the OnResume event of the Application class seems not to fire when the app is resumed by intent (seems to be a Maui bug), however Android's native OnResume works and fires correctly even when the app is resumed via intent, all you have to do is in the MainActivity class to override Android's native OnResume method:
protected override void OnResume()
{
base.OnResume();
var fromDeepLink = Preferences.Get("FromDeepLink", false);
if (fromDeepLink)
{
Preferences.Set("FromDeepLink", false);
Shell.Current.GoToAsync("myroute");
}
}
protected override void OnNewIntent(Intent intent)
{
base.OnNewIntent(intent);
var action = intent.Action;
var data = intent.DataString;
if (!string.IsNullOrWhiteSpace(data) && data.Contains("/data/"))
{
Preferences.Set("FromDeepLink", true);
}
}
I have created a custom document provider for Android using this code as a base.
https://learn.microsoft.com/en-us/samples/xamarin/monodroid-samples/storageprovider/
This allows for a new drive to be mapped onto the documents folder when browsing/saving documents.
If there is an exception due to a password timeout for example, I would like to pop back up the existing app so the users can entered their credentials again to log in.
Is this possible? As an example of what I am looking for, if the QueryRoots failed with a particular exception, could I run something to pop back up the app interface here?
public override ICursor QueryRoots(string[] projection)
{
Log.Verbose(TAG, "queryRoots");
var result = new MatrixCursor(ResolveRootProjection(projection));
try
{
if (!IsUserLoggedIn())
{
return result;
}
MatrixCursor.RowBuilder row = result.NewRow();
... other init code here
}
catch (Exception ex)
{
if (ex.Message == "NoSessionException")
{
// LOGIC TO BRING BACK APP TO LOG IN AGAIN HERE...
}
}
return result;
}
I make a sample code about how to lauch the app again for your reference. You could put Launch method in catch statement.
In Xamarin.Forms, you could use Dependency service to start the app with package name.
Create a interface:
public interface IDpendencyService
{
Task<bool> Launch(string stringUri);
}
Implemention of Android:
public class DependencyImplementation : Activity, IDpendencyService
{
public Task<bool> Launch(string stringUri)
{
Intent intent = Android.App.Application.Context.PackageManager.GetLaunchIntentForPackage(stringUri);
if (intent != null)
{
intent.AddFlags(ActivityFlags.NewTask);
Forms.Context.StartActivity(intent);
return Task.FromResult(true);
}
else
{
return Task.FromResult(true);
}
}
}
Register in MainActivity:
DependencyService.Register<IDpendencyService, DependencyImplementation>();
I use a Button event to invoke. You could try to invoke in catch.
DependencyService.Get<IDpendencyService>().Launch("com.companyname.xamarindemo");
Screenshot: I have a button on Page21. When i click the button, it would reload the app and pop back up the existing app.
How to check if any system dialog (like the one below or USSD) is displayed in Android ?
Programmatic way or cmd root way?
Any variants.
You can theoretically do this using the AccessibilityService, but it is rather complicated and may or may not work on different devices. Users will need to manually enable accessibility features for your application. You can get callbacks from Android whenever any window is opened and you can then interrogate the window to determine if it has specific text in it or belongs to a specific package, etc. This is a "brute force" approach, but it can be useful in some situations.
A system dialog is an activity. You can detect it by the top activity class name using ActivityManager.
final ActivityManager manager = (ActivityManager) context
.getSystemService(Activity.ACTIVITY_SERVICE);
In devices with API Level less than 23 (M):
final List<ActivityManager.RunningTaskInfo> runningTasks = manager.getRunningTasks(1);
final ComponentName componentName = runningTasks.get(0).topActivity;
final String className = componentName.getClassName();
if (className.equals("YOUR_EXPECTED_ACTIVITY_CLASS_NAME")) {
// do something
}
In newer devices:
final List<ActivityManager.AppTask> appTasks = manager.getAppTasks();
final ComponentName componentName = appTasks.get(0).getTaskInfo().topActivity;
final String className = componentName.getClassName();
if (className.equals("YOUR_EXPECTED_ACTIVITY_CLASS_NAME")) {
// do something
}
Or in this case, you can check if the device is in airplane mode before starting the activity:
private boolean isAirplaneModeOn(final Context context) {
final int airplaneMode = Settings.System.getInt(
context.getContentResolver(),
Settings.System.AIRPLANE_MODE_ON,
0
);
return airplaneMode != 0;
}
...
if (!isAirplaneModeOn(this)) {
// do something
}
Your question made me think of a solution in use by the permissions management in Android 6+. Have you ever seen the error message if a Toast or system alert dialog opens up when trying to set permissions?
Android "Screen Overlay Detected" message if user is trying to grant a permission when a notification is showing
The way they did it is by overriding the dispatchTouchEvent method in Activity. This can check if anything is 'in the way' intercepting touch events. You can use your special Activity as a base class for any Activity in your app that you wish to detect any overlays on it.
#Override
public boolean dispatchTouchEvent(MotionEvent event) {
mObscured = (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0;
return super.dispatchTouchEvent(event);
}
Add a public method to check at any given time if your activity is obscured
public boolean isObscured() {
return mObscured;
}
You should be careful - as it's not clear from the question - if a second Activity from a system or privileged app is at the front of the stack then your own activity will no longer be receiving touch events. This is to capture the fragments, toasts, floating widgets and other items that may share the view hierarchy.
My app is using a NotificationListener to read out messages from various 3rd party apps, for example WhatsApp.
So far I was able to send a reply if only one chat is unread, the code is below.
However, in the case with WhatsApp, getNotification().actions returns a null object when more than two chats are unread, as the messages are bundled together. As you can see in the pictures below, if the notifications are extended there is an option to send a direct reply as well, therefore I am certain that it is possible to utilize this, also I think apps like PushBullet are using this method.
How could I access the RemoteInput of that notification?
public static ReplyIntentSender sendReply(StatusBarNotification statusBarNotification, String name) {
Notification.Action actions[] = statusBarNotification.getNotification().actions;
for (Notification.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) {
if (act.title.toString().contains(name)) {
if (act.getRemoteInputs() != null)
return new ReplyIntentSender(act);
}
}
}
return null;
}
public static class ReplyIntentSender {
[...]
public final Notification.Action action;
public ReplyIntentSender(Notification.Action extractedAction) {
action = extractedAction;
[...]
}
private boolean sendNativeIntent(Context context, String message) {
for (android.app.RemoteInput rem : action.getRemoteInputs()) {
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putCharSequence(rem.getResultKey(), message);
android.app.RemoteInput.addResultsToIntent(action.getRemoteInputs(), intent, bundle);
try {
action.actionIntent.send(context, 0, intent);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
return false;
}
}
Some explanation how the above code works: Once a notification is received the app tries to get the actions and checks if the name is in the title of a remoteInput (normally it is in the format of "Reply to $NAME"), if that is found the Action is saved into a ReplyIntentSender class, which, when triggered by sendNativeIntent, cycles through all RemoteInputs of that Action and adds the message to the intent. If more than one chat is unread, getNotification().actions returns null.
Below are two screenshots, the first one where it is working without any problems and the second one where it doesn't.
You can consider this as my suggestion. I have done bit research on this and come up with following conclusions.(Also it looks like you have done plenty of research on this so it might be possible that you aware about what I wrote below)
Numerous apps send Wear specific notifications, and many of those contain actions accessible from an Android Wear device. We can grab those Wear notifications on the device, extracting the actions, finding the reply action (if one exists), populating it with our own response and then executing the PendingIntent which sends our response back the original app for it to send on to the recipient.
To do so you can refer this link (A nice workaround by Rob J). You can also refer this link in this context (Great research work done by MichaĆ Tajchert).(You might need to work around with NotificationCompat.isGroupSummary)
This is what I feel(Might be I am totally wrong)
.actions method returns Array of all Notification.Action
structures attached to current notification by addAction(int,
CharSequence, PendingIntent), Here addAction method is deprecated
one so it might not working as intended.
I am not able to test this at my end otherwise I will love to provide a working solution with code.
Hope this will help you. Happy Coding!!!
I am writing an Android service to monitor the user to check if that user left the activities or applications (and their child activities) launched by the service. I tried to use Activitymanager.getRunningTasks() to get the package name and task ID of the foreground activity and check if it is the same package that my service launched. This method works if all the child activities are stay in the same task.
However, if some activities in external applications is launched as a new task, then the above method is not working.
Is there something like "task stacks" or "application stacks" for me to trace if the foreground activity is launched by me or my child activities?
The code I tried so far, periodicCheck() is called periodically:
int myTaskId = -1;
String myPackageName = "application.launched.by.me";
void periodicCheck() {
android.app.ActivityManager activityManager = (android.app.ActivityManager) SystemManagerService.this.getSystemService(Context.ACTIVITY_SERVICE);
List<android.app.ActivityManager.RunningTaskInfo> taskInfo = activityManager.getRunningTasks(10);
String currentApplicationPackageName = taskInfo.get(0).baseActivity.getPackageName();
int currentApplicationTaskId = taskInfo.get(0).id;
if (currentApplicationPackageName.equals(myPackageName) || myTaskId == currentApplicationTaskId) {
// user is staying in our target application
// update the task ID
myTaskId = currentApplicationTaskId;
} else {
// user has left our target application, do something
}
}