I have implemented a ANR watchdog in my application using this GitHub repo. The watchdog monitors the UI thread and if it is blocked for more than 10 seconds, if restarts the app. My problem is it restarts the app twice, thus messing with all my logic.
This is my implementation of the WatchDog :
public class ANRWatchdog extends Thread {
private final Handler uiHandler = new Handler(Looper.getMainLooper());
private int count = 0; //
private static final int DEFAULT_WAIT_TIME = 10000; // in milliseconds
private volatile boolean anr = false;
private Context context;
public ANRWatchdog(Context context) {
super();
this.context = context;
}
private final Runnable counter = () -> count = (count + 1) % Integer.MAX_VALUE;
#Override
public void run() {
setName("WatchDog");
int lastCount;
while (!isInterrupted()) {
if ( anr){
anr = false;
return;
}
lastCount = count;
uiHandler.post(counter);
try {
Thread.sleep(DEFAULT_WAIT_TIME);
} catch (InterruptedException e) {
Log.e("WatchDog",
"Error while making the ANR thread sleep" + e);
return;
}
if (count == lastCount) {// means the value hasn't been incremented. UI thread has been blocked
anr = true;
Log.d("WatchDog", "Count hasn't incremented. This means ANR. Will restart the app. Thread Id : " +
android.os.Process.getThreadPriority(android.os.Process.myTid()));
uiHandler.removeCallbacks(counter, null);
uiHandler.removeCallbacksAndMessages(null);
ANRSharedPrefs.storeANR(context, true, SystemClock.elapsedRealtime());
ANRError error = ANRError.NewMainOnly();
Log.e("WatchDog", "" + error);
Log.d("WatchDog", "Now restarting the app");
RestartAppUtil.restartApp(context);
return;
}
}
}
}
Here is how the watchdog is started
public class FileLogger extends Application {
ANRWatchDog watchDog = new ANRWatchDog(this);
/**
* Called when the application is starting, before any activity, service, or receiver objects (excluding content providers) have been created.
*/
#Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Now launching Android Application : " + BuildConfig.VERSION_NAME);
File logFile = new File(ExternalStoragePath.getExternalCardPath(getApplicationContext()), "log.txt");
try {
String cmd = "logcat -v time -f " + logFile.getPath() + " TAG1:I TAG2:D TAG3:E *:S";
Runtime.getRuntime().exec(cmd);
} catch (IOException e) {
Log.e(TAG, "Exception during writing to log " + e);
}
watchDog.start();
}
}
Here is how I am restarting the app i.e RestartUtil
public static void restartApp(Context context){
context.stopService(new Intent(context, Service.class));
Intent mStartActivity = new Intent(context, MainActivity.class);
mStartActivity.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
mStartActivity.putExtra(KeyConstants.ANR, true);
int mPendingIntentId = 123456;
PendingIntent mPendingIntent = PendingIntent.getActivity(context, mPendingIntentId, mStartActivity, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100, mPendingIntent);
Runtime.getRuntime().exit(0);
}
This code works and the app is restarted.
I am simulating an ANR in one of the activities using an infinite while loop. When I do that, this is what happens in the logs
10-17 13:30:08.221 19588-19608/com.company.project D/TAG1 Count hasn't incremented. This means ANR. Will restart the app. Thread Id : 0
10-17 13:30:08.221 19588-19608/com.company.project D/TAG1 Storing the ANR time : 617417608
10-17 13:30:08.231 19588-19608/com.company.project D/TAG1 Now restarting the app
10-17 13:30:18.411 20333-20353/com.company.project D/TAG1 Count hasn't incremented. This means ANR. Will restart the app. Thread Id : 0
10-17 13:30:18.411 20333-20353/com.company.project D/TAG1 Storing the ANR time : 617427797
10-17 13:30:18.421 20333-20353/com.company.project D/TAG1 Now restarting the app
10-17 13:30:18.791 20362-20362/? D/TAG1: Getting the value of ANR time 617427797
10-17 13:30:18.791 20362-20362/? D/TAG1: Received intent in main screen
10-17 13:30:20.171 20362-20362/com.company.project D/TAG1 Getting the value of ANR time
10-17 13:30:20.171 20362-20362/com.company.project D/TAG1 Received intent in main screen 617427797
The main activity receives two intents, instead of one. Also i don't understand the presence of
/? D/TAG1
in the logs
Can anyone help me in figuring out, why the main screen gets two intents?
So I was finally able to solve this.
System.exit() was not enough in my case. I had to call finish() or finishAffinity() on the activity which was causing the ANR.
So in the onCreate()
method of every activty, I register the instance of activity in the FileLogger like this
FileLogger.setActivityName(this);
This is how the FileLogger has been modified
/**to register the activity)
public static void setActivityName(Activity activityName){
anrActivity = activityName;
}
/**This method is called by RestartUtil method to restart the app**/
public static void kill(){
if ( anrActivity != null) {
anrActivity.finish();
anrActivity.finishAffinity();
}
android.os.Process.killProcess(android.os.Process.myPid());
}
Related
I have an app that writes to its local storage depending on user actions; said contents need to
be forwarded to another app.
My approach:
create a worker thread with a file observer pointed to local storage
start worker from the apps main activity
worker thread creates and sends intents with updated contents to separate app
I'm not sure (maybe need to open a separate question), but everything created in an activity gets destroyed when the activity is stopped, right? meaning that adding workers, file observers have the same life span as the activity they're defined in, right?
Code:
MainActivity.java:
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private static final String FILE_OBSERVER_WORK_NAME = "file_observer_work";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i(TAG, "Creating file observer worker");
WorkManager workManager = WorkManager.getInstance(getApplication());
WorkContinuation continuation = workManager
.beginUniqueWork(FILE_OBSERVER_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(APIWorker.class));
Log.i(TAG, "Starting worker");
continuation.enqueue();
final Button button = findViewById(R.id.button2);
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Log.i(TAG, "Button clicked!");
String stuffToWriteToFile = getStuff();
String cwd = getApplicationInfo().dataDir;
String stuffFilePath= cwd + File.separator + "stuff.json";
PrintWriter stuffFile= null;
try {
stuffFile = new PrintWriter(stuffFilePath, "UTF-8");
stuffFile.println(stuffToWriteToFile);
stuffFile.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
});
}
#Override
public void onResume(){
super.onResume();
// start worker here?
}
#Override
public void onStart() {
super.onStart();
// start worker here?
}
}
APIWorker.java:
public class APIWorker extends Worker {
public APIWorker(#NonNull Context context, #NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
private static final String TAG = APIWorker.class.getSimpleName();
#NonNull
#Override
public Result doWork() {
Context applicationContext = getApplicationContext();
Log.d(TAG, "Observing stuff file");
FileObserver fileObserver = new FileObserver(cwd) {
#Override
public void onEvent(int event, #Nullable String path) {
if(event == FileObserver.CREATE ||
event == FileObserver.MODIFY) {
String cwd = applicationContext.getApplicationInfo().dataDir;
String stuffFilePath = cwd + File.separator + "stuff.json";
String fileContents;
File observedFile = new File(stuffFilePath);
long length = observedFile.length();
if (length < 1 || length > Integer.MAX_VALUE) {
fileContents = "";
Log.w(TAG, "Empty file: " + observedFile);
} else {
try (FileReader in = new FileReader(observedFile)) {
char[] content = new char[(int)length];
int numRead = in.read(content);
if (numRead != length) {
Log.e(TAG, "Incomplete read of " + observedFile +
". Read chars " + numRead + " of " + length);
}
fileContents = new String(content, 0, numRead);
Log.d(TAG, "Sending intent ");
String packageName = "com.cam.differentapp";
Intent sendIntent = applicationContext.getPackageManager().
getLaunchIntentForPackage(packageName);
if (sendIntent == null) {
// Bring user to the market or let them choose an app?
sendIntent = new Intent(Intent.ACTION_VIEW);
sendIntent.setData(Uri.parse("market://details?id=" + packageName));
}
// sendIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, fileContents);
sendIntent.setType("application/json");
applicationContext.startActivity(sendIntent);
Log.d(TAG, "Intent sent ");
}
catch (Exception ex) {
Log.e(TAG, "Failed to read file " + path, ex);
fileContents = "";
}
}
}
}
};
fileObserver.startWatching();
return null;
}
}
Looking at the docs:
https://developer.android.com/guide/components/activities/background-starts
there are restrictions as to when activities can be started from the background but also exceptions, namely:
The app has a visible window, such as an activity in the foreground.
meaning (I think?) that as long as the user interacts with the app (MainActivity) the background worker should run, correct? It's stopped if the activity is paused/destroyed, right?
Usually you would use a Service if you have background processing to do that doesn't need user interaction (display or user input). If your app is in the foreground then your Service can launch other activities using startActivity().
Your architecture seems very strange to me. You are using a Worker, which has a maximum 10 minute lifetime. You are starting the Worker which then creates a FileObserver to detect creation/modification of files. It then reads the file and starts another Activity. This is a very complicated and roundabout way of doing things. I have doubts that you can get this working reliably.
Your Activity is writing the data to the file system. It could just call a method (on a background thread) after it has written the file that then forwards the data to another Activity. This would be much more straightforward and has a lot less moving parts.
I don't know exactly how the lifecycle of the Activity effects the Workers. I would assume that they are not directly linked to the Activity and therefore would not stop when the Activity is paused or destroyed.
I also notice that you are writing to a file on the main (UI) thread (in your OnClickListener). This is not OK and you should do file I/O in a background thread, because file I/O can block and you don't want to block the main (UI) thread.
public class GetReceiver extends BroadcastReceiver {
#Override
public void onReceive(Context context, Intent intent) {
SharedPrefHelper sharedPrefHelper = new SharedPrefHelper(context);
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
manager.cancel(1);
if (intent != null) {
if (intent.getStringExtra("result") == "ok") {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty(Web.Register.User_Id, sharedPrefHelper.getUserId());
jsonObject.addProperty(Web.BookingRequest.ACTIVITY, "ok");
new BasePresenter<>().createApiRequest(BaseApplication.getRetrofit().create(ApisHelper.class)
.okResponse(jsonObject), new BaseCallBack<BasicApiModel>() {
#Override
public void onCallBack(BasicApiModel output) {
if (Log.isLoggable("qwert", Log.DEBUG)) {
Log.d("qwert", "Receiver's onCallBack: " + output.getMessage());
}
}
});
} else {
Log.d("qwert", "onReceive: testing_cancelled");
}
}
}
what is the correct way of starting api request from receiver ?
Don't start any long running task in your broadcast receiver, otherwise your app may crash if onReceive runs more than 10 sec.
It's better to trigger a service from onReceive method and put your network operation inside the service.
Also keep in mind to use intent service / any other service depending on your requirement, with a worker thread to run your network operation so that your UI thread doesn't get blocked.
by "restarting" the application, what I am essentially attempting to do is reset all the instance variables, fields, activities, services, and just everything the application to how it was when it was first opened. The only caveat is that I don't want to actually exit out of the app itself. Is there any way I would be accomplish this?
There's really no quick and easy solution to reset everything.
The main reason is because there's no way of knowing everything you want to reset. For example, there's a simple way to reset an Activity by relaunching the Activity and clearing the old one... however, this only resets the Activity minus the static variables. It also doesn't touch Services, Singletons, and other static values you've changed.
Only proper and reliable solution is to create a resetAll() method yourself. Have it send the reset commands to every active Service, change back default values for current Activity, null the Singletons, and etc.
What I did before on android (iOS doesn't allow to restart an app) is use a PendingIntent with a code for clearing the cache of the app. It still exits the app but it restarts it like it's from a fresh install.
cleanCache();
PackageManager pm = context.getPackageManager();
if(pm != null){
Intent activity = pm.getLaunchIntentForPackage(getBaseContext().getPackageName());
if(activity != null){
activity.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
int pendingIntentId = 223344;
PendingIntent pendingIntent = PendingIntent.getActivity(context, pendingIntentId, activity, PendingIntent.FLAG_CANCEL_CURRENT);
AlarmManager mgr = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 5, pendingIntent);
System.exit(0);
}else{
Log.d(TAG, "was not able to restart, activity null");
}
}else{
Log.d(TAG, "was not able to restart, pm null");
}
public class ExitActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exit);
android.os.Process.killProcess(android.os.Process.myPid());
}
}
Then below are the code for cleanCache().
private void cleanCache() {
clearApplicationData();
Intent intent = new Intent(context, ExitActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK |
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | Intent.FLAG_ACTIVITY_NO_ANIMATION);
context.startActivity(intent);
}
public void clearApplicationData(){
File cache = context.getCacheDir();
File appDir = new File(cache.getParent());
context.deleteDatabase("webview.db");
context.deleteDatabase("webviewCache.db");
webMain.clearCache(true);
if (appDir.exists()) {
String[] children = appDir.list();
for (String s : children) {
if (!s.equals("lib")) {
deleteDir(new File(appDir, s));Log.i("TAG", "**************** File /data/data/APP_PACKAGE/" + s + " DELETED *******************");
}
}
}
}
public static boolean deleteDir(File dir){
if (dir != null && dir.isDirectory()) {
String[] children = dir.list();
for (int i = 0; i < children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
return dir.delete();
}
Then below is the code for ExitActivity.
public class ExitActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_exit);
android.os.Process.killProcess(android.os.Process.myPid());
}
}
I am making an alarm clock which asks user to do a particular work in order to close the alarm when it rings. It is working fine but the problem is that if the user closes the alarm app from the recent activities while the alarm is ringing, the alarm stops ringing. I want that even if the user clears the app while its ringing, it should not stop ringing. It should only stop once the task given is completed. How can I implement this?
Edit #1: Activity that is called when alarm rings
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.v(LOG_TAG, "in AlarmAlert");
unlockScreen();
setContentView(R.layout.activity_alarm_alert);
Bundle bundle = this.getIntent().getExtras();
alarm = (Alarm) bundle.getSerializable("alarm");
alarmDatabase = new AlarmDatabase(this);
//Uri uri = alarm.getRingtonePath();
question = (TextView)findViewById(R.id.question);
answer = (TextView) findViewById(R.id.answer);
oldColors = answer.getTextColors();
diff = alarm.getDifficulty().toString();
questionString = GenerateMathsQuestion.generateQuestion(diff);
question.setText(questionString);
actualAnswer = EvaluateString.evaluate(questionString);
AudioManager am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
int result = am.requestAudioFocus(focusChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN);
if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mediaPlayer = new MediaPlayer();
mediaPlayer.setVolume(1.0f, 1.0f);
mediaPlayer.setLooping(true);
mediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
try {
mediaPlayer.setDataSource(this, Uri.parse(alarm.getRingtonePath()));
mediaPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
}
mediaPlayer.start();
}
if(alarm.getIsVibrate()) {
vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
long[] pattern = {1000, 200, 200, 200};
vibrator.vibrate(pattern, 0);
}
}
public void closeAlarm(){
Log.v(LOG_TAG, "will now stop");
mediaPlayer.stop();
if(vibrator!=null)
vibrator.cancel();
Log.v(LOG_TAG, "will now release");
mediaPlayer.release();
Log.v(LOG_TAG, "id of ringing alarm: " + alarm.getAlarmId());
alarm.setIsActive(false);
alarmDatabase.updateData(alarm);
cursor = alarmDatabase.sortQuery();
while(cursor.moveToNext()){
int id = cursor.getInt(cursor.getColumnIndex(AlarmDatabase.COLUMN_UID));
currentAlarm = alarmDatabase.getAlarm(id);
Log.v(LOG_TAG, "id of next alarm " + id);
if(currentAlarm != null) {
if (currentAlarm.getIsActive() == true) {
currentAlarm.scheduleAlarm(this, true);
break;
}
}
}
this.finish();
}
You should use Services. Take a look at it, that is what you want it. Generally you can make it to run an operation, and a service wont return any result. But it runs indefinitely even when you kill the app from task manager or free RAM.
I suggest this tutorial for reading about services.
UPDATE
Implement your activity with the service in the following way so it can talk with the layout and stops the alarm when required.
public class HelloService extends Service {
private Looper mServiceLooper;
private ServiceHandler mServiceHandler;
// Handler that receives messages from the thread
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
#Override
public void handleMessage(Message msg) {
// Normally we would do some work here, like download a file.
// For our sample, we just sleep for 5 seconds.
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// Restore interrupt status.
Thread.currentThread().interrupt();
}
// Stop the service using the startId, so that we don't stop
// the service in the middle of handling another job
stopSelf(msg.arg1);
}
}
#Override
public void onCreate() {
// Start up the thread running the service. Note that we create a
// separate thread because the service normally runs in the process's
// main thread, which we don't want to block. We also make it
// background priority so CPU-intensive work will not disrupt our UI.
HandlerThread thread = new HandlerThread("ServiceStartArguments",
Process.THREAD_PRIORITY_BACKGROUND);
thread.start();
// Get the HandlerThread's Looper and use it for our Handler
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
// For each start request, send a message to start a job and deliver the
// start ID so we know which request we're stopping when we finish the job
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
mServiceHandler.sendMessage(msg);
// If we get killed, after returning from here, restart
return START_STICKY;
}
#Override
public IBinder onBind(Intent intent) {
// We don't provide binding, so return null
return null;
}
#Override
public void onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
}
}
I have a service that performs an AsyncTask which calls itself after each completion. As you'll see below, I am starting my service in the foreground. It starts successfully and keeps running as intended while I have it plugged into my computer and spitting output to LogCat. I know this because to test, I have my AsyncTask loop spitting out a notification every 5 minutes. However, when I unplug it from my computer, the notifications don't come! It's as if the service just completely stops after I start it!
NOTE: My service is a regular service, not an IntentService.
Here is my onStartCommand...
#SuppressWarnings("deprecation")
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
getData(intent);
self = this;
// Enter foreground state
String title = "Service started.";
String subject = "Service is running.";
String body = "Monitoring...";
Notification notification = new Notification(R.drawable.ic_launcher, title,
System.currentTimeMillis());
if(notificationSounds)
notification.defaults |= Notification.DEFAULT_SOUND;
else
notification.sound = null;
Intent notificationIntent = new Intent(this, MainActivity3.class);
PendingIntent pendIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
notification.setLatestEventInfo(this, subject, body, pendIntent);
startForeground(1500, notification);
new BatteryLifeTask(appContext).execute();
return START_NOT_STICKY;
}
Here is my AsyncTask:
// Looping AsyncTask for continuous mode
private class BatteryLifeTask extends AsyncTask<Void, Void, Void> {
// Member variables
Context appContext;
int batteryPct0;
public BatteryLifeTask(Context context) {
super();
appContext = context;
}
protected Void doInBackground(Void... params) {
System.out.println("Entering doInBackground");
// Get the initial battery level
batteryPct0 = getBatteryPercent();
System.out.println("Initial battery percent: " + batteryPct0);
// Check time
Calendar c = Calendar.getInstance();
Date dateNow = c.getTime();
// getTime returns ms, need minutes. 60000ms in a minute.
long currTime = dateNow.getTime() / 60000;
if(currTime >= timeToUse){
finished = true;
stopSelf();
}
System.out.println("Leaving doInBackground");
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
if(!finished) {
int waitTime = 60000 * interval; // 1 minute is 60000 miliseconds
System.out.println("Entering postExecute. waitTime is " + waitTime);
Runnable r = new Runnable() {
#Override
public void run() {
if(!finished) { // In case postDelayed is pending, avoids extra notification
System.out.println("An interval has passed.");
calculateHelper(batteryPct0);
new BatteryLifeTask(appContext).execute();
}
}
};
Handler h = new Handler();
h.postDelayed(r, waitTime);
}
}
}
And here is my code for creating notifications:
// Method for creating a notification
#SuppressWarnings("deprecation")
void notify0(int id, String title, String subject, String body, boolean playSound){
NotificationManager NM = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
Notification notify = new Notification(android.R.drawable.
PendingIntent pending = PendingIntent.getActivity(
getApplicationContext(), 0, new Intent(), 0);
notify.setLatestEventInfo(getApplicationContext(), subject, body, pending);
if(playSound)
notify.defaults |= Notification.DEFAULT_SOUND;
else
notify.sound = null;
// Cancel running notification if exists
NM.cancel(id);
// Push notification
NM.notify(id, notify);
}
Can anyone help me? This is driving me insane! My app works PERFECTLY when plugged in and hooked up to USB debugging. But when unplugged, the service seems to completely halt and do nothing.
This is because you are returning START_NOT_STICKY on the Service's onStartCommand().
START_NOT_STICKY if this
service's process is killed while it is started (after returning from
onStartCommand(Intent, int, int)), and there are no new start intents
to deliver to it, then take the service out of the started state and
don't recreate until a future explicit call to
Context.startService(Intent).
You should return START_STICKY instead
START_STICKY If this service's process is killed while it is started (after returning from onStartCommand(Intent, int, int)), then
leave it in the started state but don't retain this delivered intent.
Later the system will try to re-create the service.
Check this service api changes especially the section of Service lifecycle changes.
Return START_STICKY instead of START_NOT_STICKY and review your design. In your case it's better to use the AlarmManager with a 5 minute timeout and then start an IntentService.