AlarmManagerで繰り返しのアラーム機能を実装したいのですが、バックグラウンド実行制限があります。本来の機能として定期的なバックグラウンド処理のためにはちょっと工夫が必要です。
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させて、アプリを終了。マルチタスクメニューからも削除。
夜7時頃から12時間です、ほぼ15分間隔で実行されていました、途中で多少ずれているのは不明です。完璧ではないということでしょうか。
setExactAndAllowWhileIdle:1分間隔
ついでに1分間隔でのテストもやってみました。
15分間隔のアラームを1分間隔に変更
1 2 |
// 1分毎のアラーム設定 long repeatPeriod = 1*60*1000; |
最初は1分間隔ですがあるところで9分にさせられています。またあるところで1分間隔になったりしていますがこれは端末を持って移動していた時間帯です。深いDozeから浅いDoze、あるいはStand byになったのでしょうか。
setExact:1分間隔
正確なアラーム間隔ですが、Doze 中にどうなるかです
1 2 |
alarmManager.setExact(AlarmManager.RTC_WAKEUP, startMillis, pendingIntent); |
最初の1時間は1分間隔ですが、その後間隔が伸びて約2時間、4時間、6時間間隔となっていました。Dozeの影響でしょうか
関連ページ:
- Serviceの使い方
- AlarmManagerをBroadcastRecieverと使う
- Alarm を NotificationManager で通知する
- Doze mode で AlarmManager の繰り返しアラームを実装するには
- アプリの restart
References:
サービス | Android Developers
バックグラウンド実行制限 | Android Developers
Service | Android Developers