他のアプリ画面上にアイコン画像などを表示させることが、ServiceとWindowManagerを組み合わせるとできます。
API 29
SYSTEM_ALERT_WINDOW
通常、別のアプリが起動すると、当初あったアプリはバックグラウンドに移動させられてしまいます。ここではそのバックグラウンドから画像を表示させて、後から起動したアプリの上にかぶせてみようというものです。
Serviceなので確かにバックグラウンドで長期間のタスクを実行してくれますが、システムがユーザーに電池を消費していると知らせたりしますので、アプリをアンインストールされないように注意しましょう。
- API 23からPermissionによるユーザーの許可が必要
- API 26からは使えるレイヤーがTYPE_APPLICATION_OVERLAYになりそれまでの上位レイヤーは非推奨
- API 28からはForeground Serviceでのパーミッシンが必要
Settings.canDrawOverlays()
SYSTEM_ALERT_WINDOWのPermissionの設定が必要です。
SYSTEM_ALERT_WINDOW によるとAPI23以上では明示的なユーザー許可を「permission management screen」から取得します。
intent action ACTION_MANAGE_OVERLAY_PERMISSION を投げて、 Settings.canDrawOverlays() から許可の有無を確認するようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); } ... @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { if (!Settings.canDrawOverlays(this)) { // SYSTEM_ALERT_WINDOW permission not granted... } } } |
「他のアプリに重ねて表示を許可」を尋ねる表示が出てきます。
TYPE_APPLICATION_OVERLAY
Androidの表示レーヤーはたくさんあり、それぞれ情報のpriorityによって上位から設定されています。以下レイヤー表示例
API level 26まではいくつかのレイヤーが使えたのですが、残念ながら使用することは非推奨となりました。代わりにTYPE_APPLICATION_OVERLAYを使います。
- Critical system windows: the status bar or IME
- TYPE_APPLICATION_OVERLAY
- Activity windows
レイヤー表示はWindowManager.LayoutParamsを使って設定していきます。
1 2 3 4 5 6 7 8 9 |
WindowManager.LayoutParams ( int w, // 幅 int h, // 高さ int xpos, // x位置 int ypos, // y位置 int _type, // 表示するレイヤータイプ(TYPE_SYSTEM_ALERTなど) int _flags, // viewのonTouch等の設定 int _format // pixel formatなど ) |
レイヤータイプとflagsは様々ありますのでWindowManager.LayoutParams Referenceを確認してください。
サンプルコード
常時表示するアプリとするためにServiceの startService() あるいは startForegroundService() を使い、画像としてImageViewのアイコン(少々大きめですが)にしました。
また、startしたServiceは停止させるようにしておかないとトラブルになりますので、この常駐アイコンをタッチすることにより停止できるようにします。
API 28からforeground Serviceを使う場合はPermissionをAndroidManifestに記述する必要があります。Foreground_Service | Manifest.permission
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 63 64 |
package your.package.name; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import android.annotation.TargetApi; import android.content.Intent; import android.net.Uri; import android.os.Build; import android.provider.Settings; import android.util.Log; import android.view.View; import android.widget.Button; public class MainActivity extends AppCompatActivity { public static int OVERLAY_PERMISSION_REQ_CODE = 1000; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // API 23 以上であればPermission chekを行う //if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { checkPermission(); //} // Serviceを開始するためのボタン 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); // Serviceの開始 // API26以上 startForegroundService(intent); } }); } @TargetApi(Build.VERSION_CODES.M) public void checkPermission() { if (!Settings.canDrawOverlays(this)) { Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName())); startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE); } } @TargetApi(Build.VERSION_CODES.M) @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == OVERLAY_PERMISSION_REQ_CODE) { if (!Settings.canDrawOverlays(this)) { Log.d("debug","SYSTEM_ALERT_WINDOW permission not granted..."); // SYSTEM_ALERT_WINDOW permission not granted... // nothing to do ! } } } } |
Serviceが呼ばれたら、WindowManagerにViewを追加して表示させます。
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 134 135 136 137 138 139 140 |
package your.package.name; 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.PixelFormat; import android.os.IBinder; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; public class TestService extends Service { private View view; private WindowManager windowManager; private int dpScale ; @Override public void onCreate() { super.onCreate(); // dipを取得 dpScale = (int)getResources().getDisplayMetrics().density; } @Override public int onStartCommand(Intent intent, int flags, int startId) { // startForegroundService() ----- Context context = getApplicationContext(); String channelId = "default"; String title = context.getString(R.string.app_name); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); NotificationManager notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); // Notification Channel 設定 NotificationChannel channel = new NotificationChannel( channelId, title , NotificationManager.IMPORTANCE_DEFAULT); if(notificationManager != null){ notificationManager.createNotificationChannel(channel); Notification notification = new Notification.Builder(context, channelId) .setContentTitle(title) // android標準アイコンから .setSmallIcon(android.R.drawable.btn_star) .setContentText("APPLICATION_OVERLAY") .setAutoCancel(true) .setContentIntent(pendingIntent) .setWhen(System.currentTimeMillis()) .build(); // startForeground startForeground(1, notification); } // ----- startForegroundService() // inflaterの生成 LayoutInflater layoutInflater = LayoutInflater.from(this); int typeLayer = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; windowManager = (WindowManager)getApplicationContext() .getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams params = new WindowManager.LayoutParams ( WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT, typeLayer, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, PixelFormat.TRANSLUCENT); // 右上に配置 params.gravity= Gravity.TOP | Gravity.END; params.x = 20 * dpScale; // 20dp params.y = 80 * dpScale; // 80dp // レイアウトファイルからInfalteするViewを作成 final ViewGroup nullParent = null; view = layoutInflater.inflate(R.layout.service_layer, nullParent); // ViewにTouchListenerを設定する view.setOnTouchListener(new View.OnTouchListener(){ @Override public boolean onTouch(View v, MotionEvent event) { Log.d("debug","onTouch"); if(event.getAction() == MotionEvent.ACTION_DOWN){ Log.d("debug","ACTION_DOWN"); } if(event.getAction() == MotionEvent.ACTION_UP){ Log.d("debug","ACTION_UP"); // warning: override performClick() view.performClick(); // Serviceを自ら停止させる stopSelf(); } return false; } }); // Viewを画面上に追加 windowManager.addView(view, params); return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { super.onDestroy(); Log.d("debug","onDestroy"); // Viewを削除 windowManager.removeView(view); } @Override public IBinder onBind(Intent intent) { // TODO Auto-generated method stub return null; } } |
MainActivityのレイアウトでは、サービスを起動するだけのボタンをとりあえずつくっておきますが、なくてもできないことはありません。
activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?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:gravity="center" tools:context=".MainActivity"> <Button android:id="@+id/button_start" android:text="@string/button" android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout> |
常駐するアイコンなど、OverlayするViewのレイアウトを作成します。今回はテストのためちょっと大きめの画像を張り付けてみます。
service_layer.xml
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <ImageView android:src="@drawable/umbrella" android:layout_width="wrap_content" android:layout_height="wrap_content" android:contentDescription="@string/description"/> </RelativeLayout> |
strings.xml
1 2 3 4 5 |
<resources> <string name="app_name">YourAppName</string> <string name="button">Start</string> <string name="description">image</string> </resources> |
user-permissionとServiceの記述をしておきます。
AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" ... <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- API 28 --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <application ... android:theme="@style/AppTheme"> <activity android:name=".MainActivity"> ... </activity> <service android:name=".TestService" /> </application> </manifest> |
Permissionを許可した後に、Serviceをスタートするとアイコンが表示されて、他のレイヤーの上位にいることがわかります。
startService()の場合は、しばらくするとシステムから終了させられてしまいます。startForegroundService()を使うとアプリ終了してもstatus barにアイコンが表示されてForegroundにあるアプリのようになり、シツコイ感もあります。尚、startForegroundService()はOreoからなので切り分けが必要です。
[モデル 河村友歌]
関連ページ:
References:
WindowManager.LayoutParams
SYSTEM_ALERT_WINDOW
ACTION_MANAGE_OVERLAY_PERMISSION
canDrawOverlays(android.content.Context)