AlarmManagerで繰り返しのアラーム機能を実装したいのですが、バックグラウンド実行制限があります。本来の機能として定期的なバックグラウンド処理のためにはちょっと工夫が必要です。
 
![[Android] Doze mode で AlarmManager の繰り返しアラームを実装するには 1x1.trans - [Android] Doze mode で AlarmManager の繰り返しアラームを実装するには](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
API 29
setExactAndAllowWhileIdle
Dozeモードはバッテリーの寿命を延ばすための省電力機能ですが、その制限事項として、標準AlarmMangerはメンテナンス時間枠まで保留になります。定期的なアラームは制限されるわけですが、
アラームのスケジュール設定をサポートするため、Android 6.0(API レベル 23)では、
setAndAllowWhileIdle()とsetExactAndAllowWhileIdle()という 2 つの新しいAlarmManagerメソッドが導入されています。 このメソッドを使用すると、端末が Doze モードになっていてもアラームが発生するように設定できます。
Reference: Doze と App Standby 用に最適化する | Android Developers
ということで、このsetExactAndAllowWhileIdleを実装してみます。ただし、これも9分に1回までのようです。
 
繰り返し、Repeating
AlarmManagerにはsetRepeating()というのがありますが inexact つまりばらつきがある繰り返しになってしまいます。xxxAndAllowWhileIdleにsetRepeatingというのもありません。
ある程度精度があるアラームが必要な場合はsetExact… を使うことになりますが、Serviceで可能です。
Serviceが開始したところで次のサービスをAlarmManagerで呼び出すというやり方で実現できます。
TestService.java
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | public class TestService extends Service  {     ...     @Override     public int onStartCommand(Intent intent, int flags, int startId) {         ...         // 毎回Alarmを設定する         setNextAlarmService(context);         return START_NOT_STICKY;     }     // 次のアラームの設定     private void setNextAlarmService(Context context){         // 15分毎のアラーム設定         long repeatPeriod = 15*60*1000;         Intent intent = new Intent(context, TestService.class);         long startMillis = System.currentTimeMillis() + repeatPeriod;         PendingIntent pendingIntent                 = PendingIntent.getService(context, 0, intent, 0);         AlarmManager alarmManager                 = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);         if(alarmManager != null){             // Android Oreo 以上を想定             alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,                         startMillis, pendingIntent);         }     }     ... } | 
startForegroundService()
ただ、Serviceはバックグラウンド制限のため今までのようには使えません。
Android 8.0 では、追加機能があります。システムは、バックグラウンド アプリによるバッグラウンド サービスの作成を許可しません。 そのため、Android 8.0 では、フォアグラウンドで新しいサービスを作成する
Context.startForegroundService()メソッドが新たに導入されています。
Reference: バックグラウンド実行制限 | Android Developers
BackgroundではなくForegroundだと”言い張る”ということですか、はい。
このあたりはこちらで試しています。
 
このForegroundServiceを使うためにはNotificationを使う必要があるため、通知エリアにはアイコンが表示され通知ドロワーにはアプリ名、そして通知音が鳴る(対策無しだと)ということになります。
サンプルコード
Doze中でも繰り返しができるように、15分に1回、作業は短く内部ストレージのファイルに時間を書き込むという形で作って見ました。
 
MainActivity.java
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | package your.package.name; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.content.Context; import android.content.Intent; import android.view.View; import android.widget.Button; import android.widget.TextView; public class MainActivity extends AppCompatActivity {     private TextView textView;     private InternalFileReadWrite fileReadWrite;     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);         Context context = getApplicationContext();         fileReadWrite = new InternalFileReadWrite(context);         textView = findViewById(R.id.log_text);         Button buttonStart = findViewById(R.id.button_start);         buttonStart.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 Intent intent = new Intent(getApplication(), TestService.class);                 intent.putExtra("REQUEST_CODE", 1);                 // Serviceの開始                 startForegroundService(intent);             }         });         Button buttonLog = findViewById(R.id.button_log);         buttonLog.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 textView.setText(fileReadWrite.readFile());             }         });         Button buttonStop = findViewById(R.id.button_reset);         buttonStop.setOnClickListener(new View.OnClickListener() {             @Override             public void onClick(View v) {                 Intent intent = new Intent(getApplication(), TestService.class);                 // Serviceの停止                 stopService(intent);                 fileReadWrite.clearFile();                 textView.setText("");             }         });     } } | 
 
Serviceを実行するクラスを新しく作成します。
TestService.java
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | package your.package.name; import android.app.AlarmManager; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.os.IBinder; public class TestService extends Service  {     private Context context;     @Override     public void onCreate() {         super.onCreate();         context = getApplicationContext();     }     @Override     public int onStartCommand(Intent intent, int flags, int startId) {         // 内部ストレージにログを保存         InternalFileReadWrite fileReadWrite = new InternalFileReadWrite(context);         fileReadWrite.writeFile();         int requestCode = intent.getIntExtra("REQUEST_CODE",0);         String channelId = "default";         String title = context.getString(R.string.app_name);         PendingIntent pendingIntent =                 PendingIntent.getActivity(context, requestCode,                         intent, PendingIntent.FLAG_UPDATE_CURRENT);         // ForegroundにするためNotificationが必要、Contextを設定         NotificationManager notificationManager =                 (NotificationManager)context.                         getSystemService(Context.NOTIFICATION_SERVICE);         // Notification Channel 設定         NotificationChannel channel = new NotificationChannel(                 channelId, title , NotificationManager.IMPORTANCE_DEFAULT);                 channel.setDescription("Silent Notification");                 // 通知音を消さないと毎回通知音が出てしまう                 // この辺りの設定はcleanにしてから変更                 channel.setSound(null,null);                 // 通知ランプを消す                 channel.enableLights(false);                 channel.setLightColor(Color.BLUE);                 // 通知バイブレーション無し                 channel.enableVibration(false);         if(notificationManager != null){             notificationManager.createNotificationChannel(channel);             Notification notification = new Notification.Builder(context, channelId)                     .setContentTitle(title)                     // android標準アイコンから                     .setSmallIcon(android.R.drawable.btn_star)                     .setContentText("Alarm Counter")                     .setAutoCancel(true)                     .setContentIntent(pendingIntent)                     .setWhen(System.currentTimeMillis())                     .build();             // startForeground             startForeground(1, notification);         }         // 毎回Alarmを設定する         setNextAlarmService(context);         return START_NOT_STICKY;         //return START_STICKY;         //return START_REDELIVER_INTENT;     }     // 次のアラームの設定     private void setNextAlarmService(Context context){         // 15分毎のアラーム設定         long repeatPeriod = 15*60*1000;         Intent intent = new Intent(context, TestService.class);         long startMillis = System.currentTimeMillis() + repeatPeriod;         PendingIntent pendingIntent                 = PendingIntent.getService(context, 0, intent, 0);         AlarmManager alarmManager                 = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);         if(alarmManager != null){             // Android Oreo 以上を想定             alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP,                         startMillis, pendingIntent);         }     }     private void stopAlarmService(){         Intent indent = new Intent(context, TestService.class);         PendingIntent pendingIntent = PendingIntent.getService(context, 0, indent, 0);         // アラームを解除する         AlarmManager alarmManager                 = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);         if(alarmManager != null){             alarmManager.cancel(pendingIntent);         }     }     @Override     public void onDestroy() {         super.onDestroy();         stopAlarmService();         // Service終了         stopSelf();     }     @Override     public IBinder onBind(Intent intent) {         return null;     } } | 
 
Manifest.xml
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     ...     <!-- API 28 -->     <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />     <application         ...         </activity>         <service android:name=".TestService" />     </application> </manifest> | 
 
ファイル書込み読出し用のクラスです。
InternalFileReadWrite.java
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | package your.package.name; import android.content.Context; import android.util.Log; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Locale; class InternalFileReadWrite {     private Context context;     private String FILE_NAME = "log.txt";     private StringBuffer stringBuffer;     InternalFileReadWrite(Context context){         this.context = context;     }     void clearFile(){         // ファイル削除         context.deleteFile(FILE_NAME);         // StringBuffer clear         stringBuffer.setLength(0);     }     // ファイルを保存     void writeFile() {         stringBuffer = new StringBuffer();         long currentTime = System.currentTimeMillis();         SimpleDateFormat dataFormat =                 new SimpleDateFormat("hh:mm:ss", Locale.US);         String cTime = dataFormat.format(currentTime);         Log.d("debug", cTime);         stringBuffer.append(cTime);         stringBuffer.append(System.getProperty("line.separator"));// 改行         // try-with-resources         try (FileOutputStream fileOutputstream =                      context.openFileOutput(FILE_NAME,                              Context.MODE_APPEND)){             fileOutputstream.write(stringBuffer.toString().getBytes());         } catch (IOException e) {             e.printStackTrace();         }     }     // ファイルを読み出し     String readFile() {         stringBuffer = new StringBuffer();         // try-with-resources         try (FileInputStream fileInputStream = context.openFileInput(FILE_NAME);              BufferedReader reader= new BufferedReader(                      new InputStreamReader(fileInputStream, StandardCharsets.UTF_8))         ) {             String lineBuffer;             while( (lineBuffer = reader.readLine()) != null ) {                 stringBuffer.append(lineBuffer);                 stringBuffer.append(System.getProperty("line.separator"));             }         } catch (IOException e) {             e.printStackTrace();         }         return stringBuffer.toString();     } } | 
 
レイアウトです。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:orientation="vertical"     android:background="#cdf"     tools:context=".MainActivity">     <LinearLayout         android:gravity="center"         android:background="#48f"         android:orientation="horizontal"         android:layout_margin="20dp"         android:layout_width="match_parent"         android:layout_height="wrap_content">         <Button             android:id="@+id/button_start"             android:text="@string/start"             android:textSize="18sp"             android:layout_margin="5dp"             android:layout_weight="1"             android:layout_width="0dp"             android:layout_height="wrap_content" />         <Button             android:id="@+id/button_log"             android:text="@string/log"             android:textSize="18sp"             android:layout_margin="5dp"             android:layout_weight="1"             android:layout_width="0dp"             android:layout_height="wrap_content" />         <Button             android:id="@+id/button_reset"             android:text="@string/reset"             android:textSize="18sp"             android:layout_margin="5dp"             android:layout_weight="1"             android:layout_width="0dp"             android:layout_height="wrap_content" />     </LinearLayout>     <ScrollView         android:layout_margin="20dp"         android:layout_width="match_parent"         android:layout_height="wrap_content">         <TextView             android:id="@+id/log_text"             android:textColor="#000"             android:layout_width="wrap_content"             android:layout_height="wrap_content" />     </ScrollView> </LinearLayout> | 
 
strings.xml
| 1 2 3 4 5 6 | <resources>     <string name="app_name">YourAppName</string>     <string name="start">Start</string>     <string name="log">Log</string>     <string name="reset">Reset</string> </resources> | 
結果
setExactAndAllowWhileIdle:15分間隔
これで一晩寝かせてみました。上のコードの通り15分間隔で時間を記録するアプリをstartさせて、アプリを終了。マルチタスクメニューからも削除。
 
![[Android] Doze mode で AlarmManager の繰り返しアラームを実装するには 1x1.trans - [Android] Doze mode で AlarmManager の繰り返しアラームを実装するには](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
夜7時頃から12時間です、ほぼ15分間隔で実行されていました、途中で多少ずれているのは不明です。完璧ではないということでしょうか。
setExactAndAllowWhileIdle:1分間隔
ついでに1分間隔でのテストもやってみました。
15分間隔のアラームを1分間隔に変更
| 1 2 | // 1分毎のアラーム設定 long repeatPeriod = 1*60*1000; | 
 
![[Android] Doze mode で AlarmManager の繰り返しアラームを実装するには 1x1.trans - [Android] Doze mode で AlarmManager の繰り返しアラームを実装するには](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
最初は1分間隔ですがあるところで9分にさせられています。またあるところで1分間隔になったりしていますがこれは端末を持って移動していた時間帯です。深いDozeから浅いDoze、あるいはStand byになったのでしょうか。
setExact:1分間隔
正確なアラーム間隔ですが、Doze 中にどうなるかです
| 1 2 | alarmManager.setExact(AlarmManager.RTC_WAKEUP,         startMillis, pendingIntent); | 
 
![[Android] Doze mode で AlarmManager の繰り返しアラームを実装するには 1x1.trans - [Android] Doze mode で AlarmManager の繰り返しアラームを実装するには](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
最初の1時間は1分間隔ですが、その後間隔が伸びて約2時間、4時間、6時間間隔となっていました。Dozeの影響でしょうか
関連ページ:
- Serviceの使い方
- AlarmManagerをBroadcastRecieverと使う
- Alarm を NotificationManager で通知する
- Doze mode で AlarmManager の繰り返しアラームを実装するには
- アプリの restart
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers
![[Android] Doze mode で AlarmManager の繰り返しアラームを実装するには service 00 100x100 - [Android] Doze mode で AlarmManager の繰り返しアラームを実装するには](https://akira-watson.com/wp-content/uploads/2019/06/service_00-100x100.jpg)
