Extension with Rush. Keep the app running. Doze mode. Service. Notification. Source code

Hello friends,

this topic is about keeping an app running, even if we have the screen off (black screen). We use the old trick idea of keeping the MediaPlayer running continuously. We will use codes for the Doze mode, Services, Notification and Battery.

It is the adaptation to this Community of several tutorials in Spanish on creating extensions with Rush:
1.- Create extensions with Rush.
2.- Files in asset with Rush.
3.- Keep the app running. Rush.

It is only basic and didactic information.

Testing on Android 9,

You can download:

  • Servicio.aia (Example app)
  • com.kio4.servicio.aix (Extension)
  • servicio.zip (Folder with Source code for Rush)

from: App Inventor. Creación de Servicios. Rush. Create Service. Background.

12 Likes

1.- Extension. Blocks.

6 Likes

2.- Example app.

Servicio.aia (104.9 KB)

  • When click "Start" button to execute the service. The start time and battery charge are displayed.

  • The Clock.Timer shows the time and current battery charge with a TimerInterval of 950 ms (approx. 1 sec)

  • The web http://worldtimeapi.org/api/timezone/Europe/Madrid is consulted and the time is displayed from the internet.

  • With the variable s, approximately the seconds are counted according to the TimerInterval.

  • The variable x increases, when it reaches 10, a Notification is executed. We will get a star icon in the status bar.

  • If we press the Notification, we will get on the screen the count of the variable s. Look at the LargeIcon.

rush51

  • When click "TurnOffDozemode" button, the battery optimization is ignored and the application will be able to continue connecting to the Internet to obtain the time.

  • When click "Check if if is ignoring Battery Optimizations" button we obtain whether or not battery optimization is enabled, that is, Doze mode.

7 Likes

3.- Service.

player = MediaPlayer.create(this, Uri.parse("file://"+ GetFileAsd("silencio.mp3")));
player.setLooping(true);
player.start();
  • The silencio.mp3 file is an audio file that lasts one minute, but does not produce sound, that is, it is an audio file that produces a silence sound for one minute. It works but nothing sounds (Remember S&G No one dared disturb the sound of silence)

  • If instead of this file of silencio.mp3 we put another real sound file, that file would be executed continuously in the MediaPlayer.

  • (The author of the code uses: Settings.System.DEFAULT_RINGTONE_URI)

4 Likes

4.- Doze mode. Sleeping application. Battery Optimization.

In our application, the MediaPlayer would continue executing the file silencio.mp3, but if it tries to read an internet page we would get this message:

5.- Disable Doze mode in the application. Battery optimization.

////////////////////// TURN OFF DOZE MODE ////////////////////
@SimpleFunction(description = "Turn off doze mode.")
public void TurnOffDozeMode(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Intent intent = new Intent();
String packageName = context.getPackageName();
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
if (pm.isIgnoringBatteryOptimizations(packageName)) {// if you want to disable doze mode for this package
intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
}
else { // if you want to enable doze mode
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + packageName));
}
context.startActivity(intent);
}
} 

Ummm, Google Play doesn't like this permission: "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"

6.- Battery consumption with the Doze mode disabled.

With the Doze mode disabled (Ignore Battery Optimization), our application will be able to work with the Network, WiFi, Bluetooth, ... but it will consume more battery, about 3% / hour.

  • If we have the mobile charging, connected via the USB cable to our charger, the Doze mode does not work, that is, it is not necessary to disable the Doze mode since the mobile does not go into sleep mode.

  • When the user connects the device to a power source, the system frees the apps from the inactive state, which allows them to freely access the network and execute any pending task and synchronization.

4 Likes

7.- Create the extension using Rush.

  • In C:\rush\exe\servicio\src\com\kio4\servicio
    rush43

  • In C:\rush\exe\servicio\assets are the silencio.mp3 and cara.png files, the latter we will use as LargeIcon in the Notification
    rush42

  • In C:\rush\exe\servicio\rush.yml

  # Extension icon. This can be a URL or a local image in 'assets' folder.
  icon: icon.png
  # Extension assets.
  other:
     - silencio.mp3
     - cara.png
  • In C:\rush\exe\win\servicio\src\AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.kio4.servicio">
  <application>
    <!-- <activity android:name=".MainActivity">
      <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
      </intent-filter>
    </activity> -->
	 <!-- Mention the service name here -->
	 <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
     <service android:name="com.kio4.servicio.NewService"/>
  </application>
</manifest>
7 Likes

8.- Source code.

  • Our extension consists of two files: Servicio.java and NewService.java

  • Servicio.java

package com.kio4.servicio;
// Juan A. Villalpando
// http://kio4.com/appinventor/147_extension_servicio_rush.htm
// https://www.geeksforgeeks.org/services-in-android-with-example/
// https://stackoverflow.com/questions/37568246/how-to-turn-off-doze-mode-for-specific-apps-in-marshmallow-devices-programmatica
// Compilado con Rush.

import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.annotations.UsesServices;
import com.google.appinventor.components.runtime.*;
import android.content.Context;
import android.content.Intent;

// BATTERY
import android.content.IntentFilter;
import android.os.BatteryManager;
import android.content.BroadcastReceiver;
// COPY FILE FROM ASSET
import java.io.FileOutputStream;
import	java.io.InputStream;
import	java.io.OutputStream;
import java.io.BufferedOutputStream;
import android.content.pm.PackageManager;
// DOZE MODE
import android.net.Uri;
import android.os.PowerManager;
import android.provider.Settings;
import android.os.Build;
// NOTIFICACION
import android.R;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.NotificationChannel;
import android.app.PendingIntent;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.NotificationCompat;
// LargeIcon Bitmap
import android.graphics.BitmapFactory;
import android.graphics.Bitmap;
// TIMER
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeUnit;
import android.os.CountDownTimer;

public class Servicio extends AndroidNonvisibleComponent { 

  private ComponentContainer container;
  public static Context context;
  private String actual = "----";
  private boolean running = false;
  private CountDownTimer cTimer = null;
  
  private int nivel = 0;
  private boolean activado = false;
  private boolean IgnoringBatteryOptimization = false;
  
	private int notificationId;
	private Bitmap LargeIcon;

	private  String CHANNEL_ID = "com.kio4.notificacion";
	private static final String DEFAULT_SIGNAL = "";
	private String signal = "";

  public Servicio(ComponentContainer container) {
    super(container.$form());
    this.container = container;
    context = (Context) container.$context();
	Signal(DEFAULT_SIGNAL);	
  }
  
// Obtener el valor de la Propiedad signal. GET.
@SimpleProperty(description = "Get signal value.")
public String Signal() {return signal;}

// Establecer el valor de la Propiedad signal. SET.
@DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = Servicio.DEFAULT_SIGNAL + "")
@SimpleProperty(description = "Set signal value for tap notification.")
public void Signal(String nuevoSignal) {this.signal = nuevoSignal;}
	
/////////////////// START //////////////////////////////////////////  
@SimpleFunction(description = "Start Service.")
 public void Start() {	
		 if (!activado){
			 // Copia silencio.mp3 y cara.png desde el asset de la extension al ASD.
			 try {
		     CopyFileAsset("com.kio4.servicio/silencio.mp3", GetAsdPath() + "/silencio.mp3");
			 CopyFileAsset("com.kio4.servicio/cara.png", GetAsdPath() + "/cara.png");
			 LargeIcon = BitmapFactory.decodeFile(GetAsdPath() + "/cara.png");
			 } 
			 catch (Exception e) { }
			 
			 Intent intent = new Intent(context, NewService.class);
			 context.startService(intent);
			 activado = true;
			 
		////////////////////// DEVUELVE UN TEXTO CUANDO SE PULSA LA NOTIFICACION	
		BroadcastReceiver receiver = new BroadcastReceiver() {
				public void onReceive(Context context, Intent intent) {
					  if (intent.getAction().equals("action_call_method")) {
							NotificationSignal(signal); // Desde aqui va al EVENTO y devuelve signal.
					} 
				}
			};

		IntentFilter filter = new IntentFilter("action_call_method");
		context.registerReceiver(receiver, filter);	
        }
 }
 /////// EVENTO Retorno de Notificacion.
@SimpleEvent(description = "Return notificacion.")
public void NotificationSignal(String signal){
        EventDispatcher.dispatchEvent(this, "NotificationSignal", signal);
} 

/////////////////// STOP //////////////////////////////////////////   
@SimpleFunction(description = "Stop Service.")
public void Stop() {	
   Intent intent = new Intent(context, NewService.class);
   context.stopService(intent);
   activado = false;
 }  
/////////////////////////////////////////////////////////////////////////	  
// Copia archivo desde el asset al ASD.
public void CopyFileAsset(String fileName, String dest) throws Exception {
	InputStream stream = null;
	OutputStream output = null;

    stream = context.getAssets().open(fileName);
	output = new BufferedOutputStream(new FileOutputStream(dest));

    byte data[] = new byte[1024];
    int count;

    while((count = stream.read(data)) != -1)
    {  output.write(data, 0, count);  }

    output.close();
    stream.close();
 }
//////////////////////////////// GET ASD PATH ///////////
// https://www.programcreek.com/java-api-examples/?class=android.content.Context&method=getExternalFilesDir
public String GetAsdPath() {
if (!context.getExternalFilesDir(null).exists()){context.getExternalFilesDir(null).mkdirs();}   // Crea directorio files si no existiera.
return context.getExternalFilesDir(null).getAbsolutePath();
}
////////////////////// TURN OFF DOZE MODE ////////////////////
@SimpleFunction(description  = "Turn off doze mode.")
public void TurnOffDozeMode(){
   if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Intent intent = new Intent();
            String packageName = context.getPackageName();
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            if (pm.isIgnoringBatteryOptimizations(packageName)) {// if you want to disable doze mode for this package
                intent.setAction(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
			    }
            else { // if you want to enable doze mode
                intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
                intent.setData(Uri.parse("package:" + packageName));
            }
            context.startActivity(intent);
    }
 }
/////////////////////// CHECK BATTERY OPTIMIZATION  ///////////////////////////////////
@SimpleFunction(description  = "Check if it is ignoring Battery Optimizations.")
public boolean BatteryOptimization(){
   if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String packageName = context.getPackageName();
            PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
            if (pm.isIgnoringBatteryOptimizations(packageName)) {IgnoringBatteryOptimization = true; }
            else {IgnoringBatteryOptimization = false;}
    }
	return IgnoringBatteryOptimization;
 }
///////////////////// BATERIA NIVEL ///////////////////////////////////
@SimpleFunction(description = "Battery level (percent).")
public int Battery() {
    IntentFilter iFilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
    Intent batteryStatus = context.registerReceiver(null, iFilter);

    int level = batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) : -1;
    int scale = batteryStatus != null ? batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1) : -1;

    float batteryPct = level/(float) scale;

    return (int) (batteryPct * 100);
}	
///////////////////////////////////////////// NOTIFY /////////////////////////////
@SimpleFunction(description  = "Notify in status bar.")
public void Notify(String title, String text){
final Intent intentNotification = new Intent("action_call_method");
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentNotification, PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder builderNotificationCompat = new NotificationCompat.Builder(context, CHANNEL_ID)
                .setContentIntent(pendingIntent)
                .setContentTitle(title)
                .setContentText(text)
                .setSmallIcon(android.R.drawable.btn_star_big_on)
				.setLargeIcon(LargeIcon)
		        .setAutoCancel(true); //  Notification will remove when tap.
			
        NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        // Create Notification Channel API 26+ 
		 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
        NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID, "Notificacion", NotificationManager.IMPORTANCE_DEFAULT);
        notificationManager.createNotificationChannel(notificationChannel);
		}
        notificationManager.notify(0,builderNotificationCompat.build());	
}
////////////////////////////  To Background. Minimize     //////////////////////////
// https://stackoverflow.com/questions/7530407/how-to-minimize-whole-application-in-android
@SimpleFunction(description  = "To background. Minimize this screen.")
public void ToBackground() {
Intent startMain = new Intent(Intent.ACTION_MAIN);
startMain.addCategory(Intent.CATEGORY_HOME);
startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(startMain);
}

///////////////////////////////////////////////////////////////////////////
////////////////////////////     TIMER     //////////////////////////
// Contador decremental
@SimpleFunction(description  = "Interval and tick in milliseconds.")
public void Timer(int interval, int tick) {
	CancelTimer();
    cTimer = new CountDownTimer(interval, tick) {
		
        public void onTick(long millisUntilFinished) {
			actual = Long.toString(millisUntilFinished);
			running = true;
            TimerNow(actual, running);
		}
        public void onFinish() { // Esto se realiza cuando se termina.
			running = false;
			TimerNow(actual, running); // Toque final.
        }
		
    };
    cTimer.start();
}
/////////////////////////////////////////////////////////////////////////////////////////////
// CANCELACION DEL TIMER
@SimpleFunction(description  = "Cancel Timer.")
public void CancelTimer() {
    if(cTimer!=null)
        cTimer.cancel();
}
/////////////////////// EVENTO PARA EL TIMER /////////////////////////
   @SimpleEvent(description  = "Number, is actual count of timer. Running is true or false.")
   public void TimerNow(String number, boolean running) {
    EventDispatcher.dispatchEvent(this, "TimerNow", number, running);
} 
 
} // => Fin

///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////

NewService.java

package com.kio4.servicio;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.provider.Settings;
import androidx.annotation.Nullable;

import android.net.Uri;

public class NewService extends Service {
  
    // declaring object of MediaPlayer
    private MediaPlayer player;

    @Override
  
    // execution of service will start
    // on calling this method
    public int onStartCommand(Intent intent, int flags, int startId) {
  
        // creating a media player which
        // will play the audio of Default
        // ringtone in android device
        // player = MediaPlayer.create(this, Settings.System.DEFAULT_RINGTONE_URI );
		   player = MediaPlayer.create(this, Uri.parse("file://"+ GetFileAsd("silencio.mp3")));
  
        // providing the boolean
        // value as true to play
        // the audio on loop
           player.setLooping(true);
  
        // starting the process
           player.start();
  
        // returns the status
        // of the program
        return START_STICKY;
    }
  
    @Override
  
    // execution of the service will
    // stop on calling this method
    public void onDestroy() {
        super.onDestroy();
  
        // stopping the process
        player.stop();
    }
  
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
	
//////////////////////////////// GET PATH of silencio.mp3 ///////////
public String GetFileAsd(String NameFile) {
return Servicio.context.getExternalFilesDir(null).getAbsolutePath() + "/" + NameFile;
}	
	
}  // => Fin
6 Likes

8.- Code comments.

  • The extension also has a block to check if the Doze mode is activated, that is, if the Battery Optimization is activated:

  • /////////////////////// CHECK BATTERY OPTIMIZATION //////////////////////////
    @SimpleFunction (description = "Check if it is ignoring Battery Optimizations.")
    public boolean BatteryOptimization () {

  • Another block to check the Battery level:
    ///////////////////// BATTERY LEVEL ///////////////////////////// ////////
    @SimpleFunction (description = "Battery level (percent).")
    public int Battery () {

  • We will also use a Notification. It will work for API 26+. We will use cara.png as LargeIcon.

  • The SmallIcon will be: (android.R.drawable.btn_star_big_on), we can change it to another: R.drawable  |  Android Developers

///////////////////////////////////////////// NOTIFY //// ///////////////////////////
@SimpleFunction (description = "Notify in status bar.")
public void Notify (String title, String text) {

  • When we click on the Notification, the NotificationSignal event will be executed, it will be configured through a BroadcastReceiver:

//////////////////////// RETURNS A TEXT WHEN NOTIFICATION IS PRESSED
BroadcastReceiver receiver = new BroadcastReceiver () {

  • I have also added a Timer, it is a decremental counter. It is not necessary but we have it there.
3 Likes

Apparently your extension uses a Foreground service (for the notification). Right?

Are you absolutely sure that ignoring battery optimization is actually necessary to prevent Doze mode? In my opinion, this should work without it. I did not use it in some of my apps (but only a Foreground service) and they work fine in the background / idle mode.

In my sample app Servicio.aia, I have a counter s = s + 1 and an internet call Web1.Url: http: //worldtimeapi.org ......

If I don't disable the Doze mode I get

I am talking about the battery optimization.

Doze mode is prevented by the Foreground service and not by ignoring the battery optimization. So what happens if you remove the "ignore the battery optimization" part in your extension (Java file)?

Using the TurnOffDozeMode () function, the user can ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS (// if you want to disable doze mode for this package) or ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (// if you want to enable doze mode).

If the user installs the Service.aia app and does not click the "TurnOffDozemode" button, the app enters in Dozemode because "IsIgnoringBatteryOptimization" is false.
In Dozemode works s = s + 1 but Web1.Url doesn't work.

And again: I doubt that this is responsible for preventing Doze mode, but only the Foreground service of the notification.

In other words, IGNORE_BATTERY_OPTIMIZATION has no permanent effect on Doze (especially not on certain devices, like Galaxy Note8, S8, ...). I've tested this extensively (using @Taifun's extension).

Not on my test devices (Galaxy ...).

@Anke

Battery optimization is Doze mode.
Battery optimization (Doze mode) - Taming The Droid.

I have only tried this extension installed on Xiaomi Android 9, it would be interesting for other users to try it on other versions of Android (26+)

This extension does not use FOREGROUND_SERVICE

Ok, but why doesn't Taifun's extension work then?

See e.g. here: