他のアプリ画面上にアイコン画像などを表示させることが、ServiceとWindowManagerを組み合わせるとできます。
 
![[Android] WindowManagerを使ってServiceから画像を表示させ続ける 1x1.trans - [Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
2021.2.1
SYSTEM_ALERT_WINDOW
通常、別のアプリが起動すると、当初あったアプリはバックグラウンドに移動させられてしまいます。ここではそのバックグラウンドから画像を表示させて、後から起動したアプリの上にかぶせてみようというものです。
 
Serviceなので確かにバックグラウンドで長期間のタスクを実行してくれますが、システムがユーザーに電池を消費していると知らせたりしますので、アプリをアンインストールされないように注意しましょう。
- API 23からPermissionによるユーザーの許可が必要
- API 26からは使えるレイヤーがTYPE_APPLICATION_OVERLAYになりそれまでの上位レイヤーは非推奨
- API 28からはForeground Serviceでのパーミッシンが必要
- API 30からMANAGE_OVERLAY_PERMISSION インテントはユーザーを常にシステム権限の画面に移動させられるようになった
- API 31からstartActivityForResult()とonActivityResult()が非推奨
- API 31からフォアグラウンド サービスの起動が制限されるようになった
Settings.canDrawOverlays()
Settings.canDrawOverlays() から許可の有無を確認するようにします。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | if (Settings.canDrawOverlays(this)){     //... } else{     // 許可されていない     Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,         Uri.parse("package:" + getPackageName()));     startActivity(intent);     // 設定画面に移行     launcher.launch(intent); } ActivityResultLauncher<Intent> launcher = registerForActivityResult(     new ActivityResultContracts.StartActivityForResult(), result -> {     // 許可されたか再確認     if (Settings.canDrawOverlays(this)) {         startForegroundService(intentService);     }     else{         //...     } }); | 
 
システム権限の「他のアプリに重ねて表示を許可」を尋ねる表示が出てきます。
面倒な操作が増えましたが、権限を付与する際の意識を高めることで
ユーザーを保護するのが目的だそうです
当該アプリを選択
 
![[Android] WindowManagerを使ってServiceから画像を表示させ続ける 1x1.trans - [Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
許可を与える
![[Android] WindowManagerを使ってServiceから画像を表示させ続ける 1x1.trans - [Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
許可した後は、バック2回でアプリ画面戻ります
AppOpsManagerを使うと1回にできるなど工夫はあるようです
TYPE_APPLICATION_OVERLAY
API level 26まではいくつかのレイヤーが使えたのですが、残念ながら使用することは非推奨となりました。代わりにTYPE_APPLICATION_OVERLAYを使います。これは通常のアプリの上に表示できますが、設定画面などはできません
レイヤー表示はWindowManager.LayoutParamsを使って設定していきます。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int typeLayer = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; 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); // dipを取得 int dpScale = (int)getResources().getDisplayMetrics().density; // 右上に配置 params.gravity=  Gravity.TOP | Gravity.END; params.x = 20 * dpScale; // 20dp params.y = 80 * dpScale; // 80dp | 
レイヤータイプとflagsは様々ありますのでWindowManager.LayoutParams Referenceを確認してください。
サンプルコード
常時表示するアプリとするためにServiceの startService() あるいは startForegroundService() を使い、画像としてImageViewのアイコン(少々大きめですが)にしました。
また、startしたServiceは停止させるようにしておかないとトラブルになりますので、この常駐アイコンをタッチすることにより停止できるようにします。
 
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 | //package com.example.testwindowmanager; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import android.content.Intent; import android.net.Uri; import android.provider.Settings; import android.widget.Button; import android.widget.Toast; public class MainActivity extends AppCompatActivity {     private Intent intentService;     private final ActivityResultLauncher<Intent> launcher             = registerForActivityResult(         new ActivityResultContracts.StartActivityForResult(),         result -> {             if (Settings.canDrawOverlays(this)) {                 startForegroundService(intentService);             }             else{                 Toast.makeText(getApplication(), R.string.message,                      Toast.LENGTH_LONG).show();             }         });     @Override     protected void onCreate(Bundle savedInstanceState) {         super.onCreate(savedInstanceState);         setContentView(R.layout.activity_main);         intentService = new Intent(getApplication(), TestService.class);         // Serviceを開始するためのボタン         Button buttonStart = findViewById(R.id.button_start);         buttonStart.setOnClickListener(v -> {             if (Settings.canDrawOverlays(this)){                 startForegroundService(intentService);             }             else{                 Intent intent =                      new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,                     Uri.parse("package:" + getPackageName()));                 startActivity(intent);                 launcher.launch(intent);             }         });     } } | 
 
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 | //package com.example.testwindowmanager; 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 Context context;     private NotificationManager notificationManager;     private View newView;     private WindowManager windowManager;     @Override     public void onCreate() {         super.onCreate();         context = getApplicationContext();         notificationManager = (NotificationManager)context                 .getSystemService(Context.NOTIFICATION_SERVICE);         // inflaterの生成         LayoutInflater layoutInflater = LayoutInflater.from(this);         // レイアウトファイルからInfalteするViewを作成         final ViewGroup nullParent = null;         newView = layoutInflater.inflate(R.layout.service_layer, nullParent);     }     @Override     public int onStartCommand(Intent intent, int flags, int startId) {         String channelId = "default";         String title = context.getString(R.string.app_name);         PendingIntent pendingIntent =                 PendingIntent.getActivity(context, 0, intent,                         PendingIntent.FLAG_MUTABLE);         // 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);         }         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);         // dipを取得         int dpScale = (int)getResources().getDisplayMetrics().density;         // 右上に配置         params.gravity=  Gravity.TOP | Gravity.END;         params.x = 20 * dpScale; // 20dp         params.y = 80 * dpScale; // 80dp         // ViewにTouchListenerを設定する         newView.setOnTouchListener((v, 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()                 newView.performClick();                 // Serviceを自ら停止させる                 stopSelf();             }             return false;         });         // Viewを画面上に追加         windowManager.addView(newView, params);         return super.onStartCommand(intent, flags, startId);     }     @Override     public void onDestroy() {         super.onDestroy();         Log.d("debug","onDestroy");         // Viewを削除         windowManager.removeView(newView);     }     @Override     public IBinder onBind(Intent intent) {         return null;     } } | 
マニュフェストには、
- FOREGROUND_SERVICE のPermissionをAndroidManifestに記述
- SYSTEM_ALERT_WINDOWの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> | 
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> | 
 
常駐するアイコン umbrella.png など、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> | 
Permissionを許可した後に、Serviceをスタートするとアイコンが表示されて、他のレイヤーの上位にいることがわかります。
![[Android] WindowManagerを使ってServiceから画像を表示させ続ける 1x1.trans - [Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/wp-content/themes/simplicity2/images/1x1.trans.gif)
 
startService()の場合は、しばらくするとシステムから終了させられてしまいます。startForegroundService()を使うとアプリ終了してもstatus barにアイコンが表示されてForegroundにあるアプリのようになり、シツコイ感もあります。尚、startForegroundService()はOreoからなので切り分けが必要です。
 
[モデル 河村友歌]
 
 
関連ページ:
References:
WindowManager.LayoutParams
SYSTEM_ALERT_WINDOW
ACTION_MANAGE_OVERLAY_PERMISSION
canDrawOverlays(android.content.Context)
 
![[Android] WindowManagerを使ってServiceから画像を表示させ続ける service 00 100x100 - [Android] WindowManagerを使ってServiceから画像を表示させ続ける](https://akira-watson.com/wp-content/uploads/2019/06/service_00-100x100.jpg)
