Skip to content

Instantly share code, notes, and snippets.

@mjohnsullivan
Created July 9, 2015 13:15
Show Gist options
  • Star 40 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save mjohnsullivan/403149218ecb480e7759 to your computer and use it in GitHub Desktop.
Save mjohnsullivan/403149218ecb480e7759 to your computer and use it in GitHub Desktop.
Example of how to create a long running timer service that survives activity destruction by moving to the foreground, and back to the background when a new activity bind to it.
//
// Copyright 2015 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.example.google.timerservice;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.support.v4.app.NotificationCompat;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.lang.ref.WeakReference;
/**
* Example activity to manage a long-running timer, which survives the destruction of the activity
* by using a foreground service and notification
*
* Add the following to the manifest:
* <service android:name=".MainActivity$TimerService" android:exported="false" />
*/
public class TimerActivity extends AppCompatActivity {
private static final String TAG = TimerActivity.class.getSimpleName();
private TimerService timerService;
private boolean serviceBound;
private Button timerButton;
private TextView timerTextView;
// Handler to update the UI every second when the timer is running
private final Handler mUpdateTimeHandler = new UIUpdateHandler(this);
// Message type for the handler
private final static int MSG_UPDATE_TIME = 0;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
timerButton = (Button)findViewById(R.id.timer_button);
timerTextView = (TextView)findViewById(R.id.timer_text_view);
}
@Override
protected void onStart() {
super.onStart();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Starting and binding service");
}
Intent i = new Intent(this, TimerService.class);
startService(i);
bindService(i, mConnection, 0);
}
@Override
protected void onStop() {
super.onStop();
updateUIStopRun();
if (serviceBound) {
// If a timer is active, foreground the service, otherwise kill the service
if (timerService.isTimerRunning()) {
timerService.foreground();
}
else {
stopService(new Intent(this, TimerService.class));
}
// Unbind the service
unbindService(mConnection);
serviceBound = false;
}
}
public void runButtonClick(View v) {
if (serviceBound && !timerService.isTimerRunning()) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Starting timer");
}
timerService.startTimer();
updateUIStartRun();
}
else if (serviceBound && timerService.isTimerRunning()) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Stopping timer");
}
timerService.stopTimer();
updateUIStopRun();
}
}
/**
* Updates the UI when a run starts
*/
private void updateUIStartRun() {
mUpdateTimeHandler.sendEmptyMessage(MSG_UPDATE_TIME);
timerButton.setText(R.string.timer_stop_button);
}
/**
* Updates the UI when a run stops
*/
private void updateUIStopRun() {
mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
timerButton.setText(R.string.timer_start_button);
}
/**
* Updates the timer readout in the UI; the service must be bound
*/
private void updateUITimer() {
if (serviceBound) {
timerTextView.setText(timerService.elapsedTime() + " seconds");
}
}
/**
* Callback for service binding, passed to bindService()
*/
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Service bound");
}
TimerService.RunServiceBinder binder = (TimerService.RunServiceBinder) service;
timerService = binder.getService();
serviceBound = true;
// Ensure the service is not in the foreground when bound
timerService.background();
// Update the UI if the service is already running the timer
if (timerService.isTimerRunning()) {
updateUIStartRun();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Service disconnect");
}
serviceBound = false;
}
};
/**
* When the timer is running, use this handler to update
* the UI every second to show timer progress
*/
static class UIUpdateHandler extends Handler {
private final static int UPDATE_RATE_MS = 1000;
private final WeakReference<TimerActivity> activity;
UIUpdateHandler(TimerActivity activity) {
this.activity = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message message) {
if (MSG_UPDATE_TIME == message.what) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "updating time");
}
activity.get().updateUITimer();
sendEmptyMessageDelayed(MSG_UPDATE_TIME, UPDATE_RATE_MS);
}
}
}
/**
* Timer service tracks the start and end time of timer; service can be placed into the
* foreground to prevent it being killed when the activity goes away
*/
public static class TimerService extends Service {
private static final String TAG = TimerService.class.getSimpleName();
// Start and end times in milliseconds
private long startTime, endTime;
// Is the service tracking time?
private boolean isTimerRunning;
// Foreground notification id
private static final int NOTIFICATION_ID = 1;
// Service binder
private final IBinder serviceBinder = new RunServiceBinder();
public class RunServiceBinder extends Binder {
TimerService getService() {
return TimerService.this;
}
}
@Override
public void onCreate() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Creating service");
}
startTime = 0;
endTime = 0;
isTimerRunning = false;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Starting service");
}
return Service.START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Binding service");
}
return serviceBinder;
}
@Override
public void onDestroy() {
super.onDestroy();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Destroying service");
}
}
/**
* Starts the timer
*/
public void startTimer() {
if (!isTimerRunning) {
startTime = System.currentTimeMillis();
isTimerRunning = true;
}
else {
Log.e(TAG, "startTimer request for an already running timer");
}
}
/**
* Stops the timer
*/
public void stopTimer() {
if (isTimerRunning) {
endTime = System.currentTimeMillis();
isTimerRunning = false;
}
else {
Log.e(TAG, "stopTimer request for a timer that isn't running");
}
}
/**
* @return whether the timer is running
*/
public boolean isTimerRunning() {
return isTimerRunning;
}
/**
* Returns the elapsed time
*
* @return the elapsed time in seconds
*/
public long elapsedTime() {
// If the timer is running, the end time will be zero
return endTime > startTime ?
(endTime - startTime) / 1000 :
(System.currentTimeMillis() - startTime) / 1000;
}
/**
* Place the service into the foreground
*/
public void foreground() {
startForeground(NOTIFICATION_ID, createNotification());
}
/**
* Return the service to the background
*/
public void background() {
stopForeground(true);
}
/**
* Creates a notification for placing the service into the foreground
*
* @return a notification for interacting with the service when in the foreground
*/
private Notification createNotification() {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setContentTitle("Timer Active")
.setContentText("Tap to return to the timer")
.setSmallIcon(R.mipmap.ic_launcher);
Intent resultIntent = new Intent(this, TimerActivity.class);
PendingIntent resultPendingIntent =
PendingIntent.getActivity(this, 0, resultIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
return builder.build();
}
}
}
@neelkadia-zz
Copy link

Can you paste an example of its use?

@rahulnag
Copy link

it is not showing anything in textView, I don't know why......if u know then please tell me

@rahulnag
Copy link

worst code.....

@NotYeshwanthReddy
Copy link

It's Not Working...(It's just un-responsive on start button click.)

@nsakshay99
Copy link

Its working fine and also i have one doubt can we do this in IntentService instead of Service.
Now it is running in Service means in main Thread If this can be done in IntentService it will run in worker Thread, Performance counts.
Can you please help me how to do in IntentService.
Thank you.

@jaydangar
Copy link

Working Great...

@arunstar
Copy link

Don't forget to register the service in Manifest.

<service android:name=".TimerActivity$TimerService" />

@selmi-karim
Copy link

worst code

@vigneshu28
Copy link

Working Great...

how to use it i need the full code

@peterpazmandi
Copy link

I like this approach, because it works fine. I have tested for 6 hours, and it didn't stop, or slow down. Thanks ;)

@musclebananas
Copy link

musclebananas commented Mar 3, 2020

Great Code, But! Only works with small changes in Android 8.1 and above.

  1. Grant permission
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

  2. Create a notification channel like this

     `  @RequiresApi(Build.VERSION_CODES.O)
     private fun createNotificationChannel(channelId: String, channelName: String) : String {
         val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_NONE)
         chan.lightColor = getColor(R.color.grayTextDarker)
         chan.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
         
         val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
         service.createNotificationChannel(chan)
         return channelId
     }  `
    
  3. Check version

    ` private fun createNotification() : Notification {
         val channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                 createNotificationChannel("my_service", "My Background Service")
             } else {
                 ""
             }
    
         val builder = NotificationCompat.Builder(this, channelId)
             .setContentTitle("Timer Active")
             .setContentText("Tap to return to the timer")
             .setSmallIcon(R.mipmap.ic_launcher)
    
         val resultIntent = Intent(this, LessonActivity::class.java)
         val resultPendingIntent =
         PendingIntent.getActivity(this, 0, resultIntent,
             PendingIntent.FLAG_UPDATE_CURRENT)
         builder.setContentIntent(resultPendingIntent)
    
         return builder.build()
     }`
    

Solution from here:
https://stackoverflow.com/questions/47531742/startforeground-fail-after-upgrade-to-android-8-1

@peterpazmandi
Copy link

As you can see above, this code was written 5 year ago, when Android 6.0 was released, so I think we can "forgive" this little thing for the writer ;)

@dharmiksit
Copy link

New Update?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment