Xamarin Android: get one app's state information from another - android

I have two Xamarin Android apps -- let's call them "Archy" and "Mehitabel".
Archy has some persistent state information (which, let's say for the sake of argument, is in a SQLite DB).
If a certain thing happens to Mehitabel, she needs to know a piece of that state information.
To accomplish this feat, I have Mehitabel send an intent to Archy. Archy has a broadcast receiver that hears it, gathers the necessary state, and fires a different intent back to Mehitabel.
Here's the code from Archy:
[BroadcastReceiver(Enabled = true)]
[IntentFilter(new [] { "com.example.Archy.SendStateToMehitabel"})]
public class StateQueryReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
var msg = new Intent("com.example.Mehitabel.StateFromArchy");
msg.PutExtra("ImportantStateInfo", GetSomeState());
context.SendBroadcast(msg);
}
}
And here's the code from Mehitabel:
private async Task AskArchyForState()
{
var filter = new IntentFilter("com.example.Mehitabel.StateFromArchy");
var csrc = new TaskCompletionSource<bool>();
var rcvr = new ActionBroadcastReceiver((context, intent) =>
{
State = intent.GetStringExtra("ImportantStateInfo");
csrc.TrySetResult(State != null);
});
RegisterReceiver(rcvr, filter);
var msg = new Intent("com.example.Archy.SendStateToMehitabel");
SendBroadcast(msg);
var task = await Task.WhenAny(csrc.Task, Task.Delay(Timeout));
UnregisterReceiver(rcvr);
if (task != csrc.Task)
bomb("Archy has not answered state query after {0}ms", Timeout);
if (!csrc.Task.IsCompletedSuccessfully || csrc.Task.Result == false)
bomb("failed to get all necessary state from Archy");
}
That all works great, provided Archy is actually running (i.e., shown in the "recent" list). If Archy isn't running, Archy's receiver code is never executed and Mehitabel times out.
My hope is I'm missing something simple (like a flag in one of the receiver attributes, or some secret sauce in the com.example.Archy.SendStateToMehitabel intent).
Can you tell me what I'm missing here?
Do I need to be using a completely different approach (like having Mehitabel StartActivityForResult() an activity within Archy, or using a service that starts on boot and runs all the time)?

Based on my research, I think you could open Archy before you need the data in Mehitabel. This is demo about opening an app in code.
Intent launchIntent = PackageManager.GetLaunchIntentForPackage("NewTestApp.NewTestApp");
if (launchIntent != null)
{
StartActivity(launchIntent);
}
Note:NewTestApp.NewTestApp is package name of Archy.

Related

Testing Activity Recognition Transition API

I am currently developing an app which uses Activity Recognition Transition API (the new one, not the old, check the link below). My question is, how can I test my app? More exactly, how can I "manually" trigger transition events? Do I really have to put my phone and laptop into my backpack and go for a ride on a bicycle to trigger the ON_BYCICLE/ACTIVITY_TRANSITION_ENTER event? There must be an easier way :) Maybe using adb? https://developer.android.com/studio/test/command-line
API documentation: https://developer.android.com/guide/topics/location/transitions
You can use following code to emulate the event
var intent = Intent()
// Your broadcast receiver action
intent.action = BuildConfig.APPLICATION_ID + "TRANSITIONS_RECEIVER_ACTION"
var events: ArrayList<ActivityTransitionEvent> = arrayListOf()
// You can set desired events with their corresponding state
var transitionEvent = ActivityTransitionEvent(DetectedActivity.IN_VEHICLE, ActivityTransition.ACTIVITY_TRANSITION_ENTER, SystemClock.elapsedRealtimeNanos())
events.add(transitionEvent)
var result = ActivityTransitionResult(events)
SafeParcelableSerializer.serializeToIntentExtra(result, intent, "com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_RESULT")
activity?.sendBroadcast(intent)
I have created simple activity with two buttons in my application where I broadcast these events(Start and Stop) respectively. This helps me debug the application gracefully.
I don't know of any way to simulate a transition event from outside your app. But within your app, you can construct a suitable Intent and send it to your receiver.
Add this method to any Context (e.g. an Activity or Service): (Kotlin)
class MyService: Service() {
fun sendFakeActivityTransitionEvent() {
// name your intended recipient class
val intent = Intent(this, MyReceiver::class.java)
val events: ArrayList<ActivityTransitionEvent> = arrayListOf()
// create fake events
events.add(
ActivityTransitionEvent(
DetectedActivity.ON_BICYCLE,
ActivityTransition.ACTIVITY_TRANSITION_ENTER,
SystemClock.elapsedRealtimeNanos()
)
)
// finally, serialize and send
val result = ActivityTransitionResult(events)
SafeParcelableSerializer.serializeToIntentExtra(
result,
intent,
"com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_RESULT"
)
this.sendBroadcast(intent)
}
}
Substitute your desired recipient for MyReceiver -- any subclass of BroadcastReceiver should work.
Then call sendFakeActivityTransitionEvent() when desired.

Pass string information from Fragment to BroadcastReceiver

I am using ACTION_UNINSTALL_PACKAGE to uninstall packages and am trying to retrieve the application name of the removed package after it is removed. I can only seem to get the package name. I cannot use ApplicationInfo on the package name since the package is already gone. I tried passing the value into the intent but since it goes to another activity that is not owned by me UninstallerActivity it is not there. I couldn't figure out how to pass the string into the IntentFilter data field since I need to use it for package name. I am not using sendBroadcast so I cannot use that.
#Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE);
intent.setData(Uri.parse("package:"+packageName));
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction("android.intent.action.PACKAGE_REMOVED");
intentFilter.addDataScheme("package");
mContext.registerReceiver(mUninstallReceiver, intentFilter);
startActivity(intent);
}
private BroadcastReceiver mUninstallReceiver = new BroadcastReceiver(){
#Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action != null) {
if (action.equals(Intent.ACTION_PACKAGE_REMOVED)) {
Uri intentData = intent.getData();
//the above only contains package name
}
}
}
}
How do I pass the application name to this broadcast receiver?
Update
I ended up just using a member variable which isn't what I totally wanted, but it works.
How do I pass the application name to this broadcast receiver?
Define your own custom subclass of BroadcastReceiver, where you supply a PackageManager to the constructor
Have that constructor gather whatever information it needs from the PackageManager
Create an instance of that BroadcastReceiver before you call startActivity()
Hold onto that BroadcastReceiver object somewhere so you can unregister it as soon as you receive your broadcast
If there is a possibility that the user might request to uninstall 2+ apps before the first uninstall completes, have onReceive() confirm that the broadcast it received is for the package it is tracking, as with this plan, you will have 2+ BroadcastReceiver objects outstanding at any point in time
There are other possible ways of organizing this (e.g., Map of package name to data, so you only need one receiver), but they will all be along the same lines: collect the data you need before you uninstall, so that you have the data by the time you receive the broadcast.

Can I see active/pending syncs in :sync process from main process?

Is it possible to determine (from the process an app was started in) sync status for a SyncAdapter that is running in a separate :sync process? I've been toying with the standard ContentResolver methods below and can't get any of them to return true unless the code below executes in the same process as my SyncAdapter (the :sync process).
val currentSyncs = ContentResolver.getCurrentSyncs().any { it.authority == <authority> }
val syncPending = ContentResolver.isSyncPending(account, <authority>)
val syncActive = ContentResolver.isSyncActive(account, <authority>)
None of the sync framework documentation seems to indicate that this isn't possible in cross-process scenarios, so I'm a bit stumped, but it seems like the most likely explanation for this.
I was rather unlucky trying to find a reliable way to get sync status information from ContentResolver.
What worked for me: Use intents to broadcast information from your SyncAdapter to another component:
// when sync started
Intent i = new Intent();
i.setAction(MyIntents.INTENT_ACTION_SYNC_STARTED);
context.sendBroadcast(i);
// when sync completed
Intent i = new Intent();
i.setAction(MyIntents.INTENT_ACTION_SYNC_DONE);
context.sendBroadcast(i);
and then receive the intents:
registerReceiver(new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, 'Sync started', Toast.LENGTH_LONG).show();
}
}, new IntentFilter(MyIntents.INTENT_ACTION_SYNC_STARTED));

How to get the class name that fired an Activity? [duplicate]

Is there a way for an Activity to find out who (i.e. class name) has sent an Intent? I'm looking for a generic way for my Activity to respond to a received intent by sending one back to the sender, whoever that may be.
There may be another way, but the only solution I know of is having Activity A invoke Activity B via startActivityForResult(). Then Activity B can use getCallingActivity() to retrieve Activity A's identity.
Is it an external app you receive the intent from? You could use the getReferrer() method of the activity class
A simple example: I opened google map app to share some location with my app by using the share option of google maps. Then my app opens and this method call in the Activity:
this.getReferrer().getHost()
will return:
com.google.android.apps.maps
see documentation here: https://developer.android.com/reference/android/app/Activity.html#getReferrer()
Note that this requires API 22. For older Android versions see answer from ajwillliams
A technique I use is to require the application sending the relevant Intent to add a PendingIntent as a Parcelable extra; the PendingIntent can be of any type (service, broadcast, etc.). The only thing my service does is call PendingIntent.getCreatorUid() and getCreatorPackage(); this information is populated when the PendingIntent is created and cannot be forged by the app so I can get the info about an Intent's sender.
Only caveat is that solution only works from Jellybean and later which is my case.
Hope this helps,
This isn't incredibly direct but you can get a list of the recent tasks from ActivityManager. So the caller would essentially be the task before yours and you can fetch info on that task.
Example usage:
ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(10000,ActivityManager.RECENT_WITH_EXCLUDED);
The above will return a list of all the tasks from most recent (yours) to the limit specified. See docs here for the type of info you can get from a RecentTaskInfo object.
Generally you don't need to know this. If the calling activity uses startActivityForResult(Intent, int), the callee can use setResult(int, Intent) to specify an Intent to send back to the caller. The caller will receive this Intent in its onActivityResult(int, int, Intent) method.
Based on your question, since you want to send an intent back to the sender startActivityForResult is a better choice than what I am going to suggest. But I needed to start activity B when a notification is clicked by the user and execute some code in activity B only if the sender activity is activity A. This is how I did it quite simply.
Inside Activity A:
String senderName = this.getClass().getSimpleName();
Intent clickIntent = new Intent(ActivityA.this, ActivityB.class);
clickIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
clickIntent.putExtra("SENDER_CLASS_NAME", senderName);
//I use PendingIntent to start Activity B but you can use what you like such as this.startActivity(clickIntent);
PendingIntent.getActivity(ActivityA.this, NOTIFICATION_ID, clickIntent, PendingIntent.FLAG_ONE_SHOT);
Inside Activity B:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
if(bundle.containsKey("SENDER_CLASS_NAME")){
String senderName = bundle.getString("SENDER_CLASS_NAME");
//Execute some code
Log.d("GCM", "Notifications clicked");
}
}
}
}
In my case, neither the accepted here and another most voted answer works perfectly.
Activity.getCallerActivity() works only for the sender which starts your activity by startActivityForResult, meaning that if the sender is also in your app and you have full control, it works, but not every external app starts others in that way.
Another most voted answer provides the solution for external app, but it too has issue. First I would prefer getAuthority() instead of getHost(), secondly, if the sender is a browser kind of app, like Chrome, both host and authority will give you the browsing web page's address host, such as www.google.com, instead of the app itself. So it depends on how you define 'sender', if you need to find out which web page starts you, the authority/host is good enough, but if you need to find out which app starts you, I am afraid authority/host can be trusted only when getScheme() gives you android-app instead of http.
Use UsageStatsManager and the old RecentTaskInfo to get the intent sender for OnCreate or onNewIntent:
public static String getTopMostThirdPartyPackage(Context context) {
String thisPak = null, tmp, top = null;
try {
thisPak = context.getPackageName();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
UsageStatsManager man = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
long now = System.currentTimeMillis();
UsageEvents uEvts = man.queryEvents(now - 5000,now); // query in 5 sec
UsageEvents.Event e = new UsageEvents.Event();
while (uEvts.getNextEvent(e)){
tmp = e.getPackageName();
if (!thisPak.equals(tmp)) {
top = tmp;
break;
}
}
} else {
ActivityManager man = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RecentTaskInfo> tasks = man.getRecentTasks(3, 0);
for(ActivityManager.RecentTaskInfo info:tasks) {
tmp = info.baseIntent.getComponent().getPackageName();
if (!thisPak.equals(tmp)) {
top = tmp;
break;
}
}
}
} catch (Exception e) {
}
return top;
}
permissions :
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<uses-permission android:name="android.permission.GET_TASKS" />
intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);

How to get the sender of an Intent?

Is there a way for an Activity to find out who (i.e. class name) has sent an Intent? I'm looking for a generic way for my Activity to respond to a received intent by sending one back to the sender, whoever that may be.
There may be another way, but the only solution I know of is having Activity A invoke Activity B via startActivityForResult(). Then Activity B can use getCallingActivity() to retrieve Activity A's identity.
Is it an external app you receive the intent from? You could use the getReferrer() method of the activity class
A simple example: I opened google map app to share some location with my app by using the share option of google maps. Then my app opens and this method call in the Activity:
this.getReferrer().getHost()
will return:
com.google.android.apps.maps
see documentation here: https://developer.android.com/reference/android/app/Activity.html#getReferrer()
Note that this requires API 22. For older Android versions see answer from ajwillliams
A technique I use is to require the application sending the relevant Intent to add a PendingIntent as a Parcelable extra; the PendingIntent can be of any type (service, broadcast, etc.). The only thing my service does is call PendingIntent.getCreatorUid() and getCreatorPackage(); this information is populated when the PendingIntent is created and cannot be forged by the app so I can get the info about an Intent's sender.
Only caveat is that solution only works from Jellybean and later which is my case.
Hope this helps,
This isn't incredibly direct but you can get a list of the recent tasks from ActivityManager. So the caller would essentially be the task before yours and you can fetch info on that task.
Example usage:
ActivityManager am = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
List<ActivityManager.RecentTaskInfo> recentTasks = am.getRecentTasks(10000,ActivityManager.RECENT_WITH_EXCLUDED);
The above will return a list of all the tasks from most recent (yours) to the limit specified. See docs here for the type of info you can get from a RecentTaskInfo object.
Generally you don't need to know this. If the calling activity uses startActivityForResult(Intent, int), the callee can use setResult(int, Intent) to specify an Intent to send back to the caller. The caller will receive this Intent in its onActivityResult(int, int, Intent) method.
Based on your question, since you want to send an intent back to the sender startActivityForResult is a better choice than what I am going to suggest. But I needed to start activity B when a notification is clicked by the user and execute some code in activity B only if the sender activity is activity A. This is how I did it quite simply.
Inside Activity A:
String senderName = this.getClass().getSimpleName();
Intent clickIntent = new Intent(ActivityA.this, ActivityB.class);
clickIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
clickIntent.putExtra("SENDER_CLASS_NAME", senderName);
//I use PendingIntent to start Activity B but you can use what you like such as this.startActivity(clickIntent);
PendingIntent.getActivity(ActivityA.this, NOTIFICATION_ID, clickIntent, PendingIntent.FLAG_ONE_SHOT);
Inside Activity B:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState == null) {
Bundle bundle = getIntent().getExtras();
if (bundle != null) {
if(bundle.containsKey("SENDER_CLASS_NAME")){
String senderName = bundle.getString("SENDER_CLASS_NAME");
//Execute some code
Log.d("GCM", "Notifications clicked");
}
}
}
}
In my case, neither the accepted here and another most voted answer works perfectly.
Activity.getCallerActivity() works only for the sender which starts your activity by startActivityForResult, meaning that if the sender is also in your app and you have full control, it works, but not every external app starts others in that way.
Another most voted answer provides the solution for external app, but it too has issue. First I would prefer getAuthority() instead of getHost(), secondly, if the sender is a browser kind of app, like Chrome, both host and authority will give you the browsing web page's address host, such as www.google.com, instead of the app itself. So it depends on how you define 'sender', if you need to find out which web page starts you, the authority/host is good enough, but if you need to find out which app starts you, I am afraid authority/host can be trusted only when getScheme() gives you android-app instead of http.
Use UsageStatsManager and the old RecentTaskInfo to get the intent sender for OnCreate or onNewIntent:
public static String getTopMostThirdPartyPackage(Context context) {
String thisPak = null, tmp, top = null;
try {
thisPak = context.getPackageName();
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
UsageStatsManager man = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
long now = System.currentTimeMillis();
UsageEvents uEvts = man.queryEvents(now - 5000,now); // query in 5 sec
UsageEvents.Event e = new UsageEvents.Event();
while (uEvts.getNextEvent(e)){
tmp = e.getPackageName();
if (!thisPak.equals(tmp)) {
top = tmp;
break;
}
}
} else {
ActivityManager man = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RecentTaskInfo> tasks = man.getRecentTasks(3, 0);
for(ActivityManager.RecentTaskInfo info:tasks) {
tmp = info.baseIntent.getComponent().getPackageName();
if (!thisPak.equals(tmp)) {
top = tmp;
break;
}
}
}
} catch (Exception e) {
}
return top;
}
permissions :
<uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
<uses-permission android:name="android.permission.GET_TASKS" />
intent = new Intent(Settings.ACTION_USAGE_ACCESS_SETTINGS);
startActivity(intent);

Categories

Resources