Androidアプリ開発

他のアプリのスクリーンショットを取得する
MediaProjection

この記事は約37分で読めます。
記事内に広告が含まれています。
スポンサーリンク

この記事は Androidスマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


実行中の他のアプリ画面のスクリーンショットを取得する

マクセル CPRM対応 DVD-R 120分 16倍速対応 50枚

ポイント

Andoidではアプリを起動すると、画面に表示しているアプリはバックグラウンドに移動します。
このため他のアプリを操作している動画や画面のスクリーンショットを取得することはできません。
Android 5で画面に表示している内容をメディアストリームとしてキャプチャするMediaProjectionが追加されました。
今回はMediaProjectionを使用して、実行中の他のアプリ画面のスクリーンショットを取得する実装を紹介します。

メディア プロジェクション

SYSTEM_ALERT_WINDOW

他のアプリ画面のスクリーンショットを取得するには、他のアプリの上にも操作パネルを表示する必要があります。
また、他のアプリ画面の上にViewを表示するためには、SYSTEM_ALERT_WINDOW権限とユーザ承認が必要です。
権限はマニフェストに記述、ユーザ承認はスクリーンショットを取得するサービスの起動前に行います。

システム アラート ウィンドウの変更

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    :
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
    :
    <application
        :
        <service
            android:name=".service.SnapService"
            android:foregroundServiceType="mediaProjection" />
        :
    </application>
</manifest>

他のアプリの上にViewを表示する場合、Serviceで実装する必要があります。
マニフェストに追加する権限は、Serviceの実装に必要なPOST_NOTIFICATIONS権限、他のアプリ画面の上にViewを表示するためのSYSTEM_ALERT_WINDOW権限、アプリの画面のスナップショットを取得するためのFOREGROUND_SERVICE_MEDIA_PROJECTION権限を追加します。
ServiceforegroundServiceTypemediaProjectionを設定します。

Activity : ユーザ承認とcreateScreenCaptureIntent

Android13以上ではServiceを起動する場合にPOST_NOTIFICATIONS権限が必要です。
メニュー(ActionMenuView)のキャプチャを取得する(アプリの画面のスナップショットを取得する)のタップで、POST_NOTIFICATIONSのユーザ承認を確認します。
ユーザ承認を取得(Android12以前は確認不要)している場合、MediaProjectionManagerIntentを取得します。
取得したIntentにクラス(SnapService.class)をセットして、Serviceを起動します。

public class MainActivity extends Utility {
    private static final int        REQUEST_MULTI_PERMISSIONS = 301;
    private MediaProjectionManager  mediaProjectionManager;
    private ActionMenuView          actionMenuView;
    private BroadcastReceiver       broadcastReceiver;
    private LocalBroadcastManager   localBroadcastManager;
    :
    // ActivityResultLauncher //
    private final ActivityResultLauncher<Intent> activityResultLauncher= registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == Activity.RESULT_OK) {
                    :
                    switch (menu) {
                        :
                        case 8: // キャプチャを取得する
                            if (result.getData() != null) {
                                capture(result.getData());
                            }
                            break;
                            :
                    }
                } else if (menu == 8) {
                    mediaProjectionManager = null;
                }
            });
    :
    // キャプチャを取得するサービスの起動
    private void capture(Intent intent) {
        intent.setClass(this, SnapService.class);
        intent.setAction(SnapService.START);
        startForegroundService(intent);
        moveTaskToBack(true);
    }
    :
    private void checkPermissions() {
        ArrayList<String> requestPermissions = new ArrayList<>();
        // 通知(33以上)
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) {
            if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions.add(Manifest.permission.POST_NOTIFICATIONS);
            }
        }
        if (!requestPermissions.isEmpty()) {
            ActivityCompat.requestPermissions(this, requestPermissions.toArray(new String[0]), REQUEST_MULTI_PERMISSIONS);
        } else {
            mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
            activityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent());
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (DEBUG) Log.d(TAG, "onRequestPermissionsResult");
        if (requestCode == REQUEST_MULTI_PERMISSIONS) {
            if (grantResults.length > 0) {
                for (int i = 0; i < permissions.length; i++) {
                    if (permissions[i].equals(Manifest.permission.POST_NOTIFICATIONS)) {
                        if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
                            mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
                            activityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent());
                        }
                    }
                }
            }
        }
    }

    protected void onCreate(Bundle savedInstanceState) {
        :
        // メニュー
        actionMenuView = findViewById(R.id.menu);
        actionMenuView.setVisibility(View.VISIBLE);
        actionMenuView.getMenu().removeGroup(Menu.NONE);
        :
        actionMenuView.getMenu().add(Menu.NONE, 8, Menu.NONE, context.getString(R.string.menu_snap));
        actionMenuView.setOnMenuItemClickListener(menuItem -> {
                menu = menuItem.getItemId();
                Intent intent;
                switch (menu) {
                    :
                    case 8: // キャプチャを取得する
                        if (!Settings.canDrawOverlays(this)) {
                            intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName"));
                            activityResultLauncher.launch(intent);
                        }
                        checkPermissions();
                        break;
                        :
                }
            }
            return false;
        });
        :
        // サービス通信用レシーバ設定
        if (broadcastReceiver == null) {
            broadcastReceiver = new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                    if (intent.getLongExtra("ALIVE", 0) > 0) {
                        actionMenuView.setVisibility(View.INVISIBLE);
                    } else {
                        actionMenuView.setVisibility(View.VISIBLE);
                    }
                }
            };
            localBroadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());
            final IntentFilter filter = new IntentFilter();
            filter.addAction("SEND_MESSAGE");
            localBroadcastManager.registerReceiver(broadcastReceiver, filter);
        }
    }
    :
    protected void onDestroy() {
        localBroadcastManager.unregisterReceiver(broadcastReceiver);
        Intent intent = new Intent(this, SnapService.class);
        intent.setAction(SnapService.STOP);
        stopService(intent);
        super.onDestroy();
    }
    :
}

Serviceの起動後、ActivitymoveTaskToBackでバックグラウンドで待機します。
また、Service実行中はBroadcastReceiverServiceからのALIVE通信を監視、多重起動ができないようにします。
Activityの終了(onDestroy)で同時にServiceも終了します。

ACTION_MANAGE_OVERLAY_PERMISSIONのユーザ承認画面(Android11)

Service : スクリーンショットの取得

Activityで取得したMediaProjectionManagerIntentからMediaProjectionを取得します。
MediaProjectionが取得できたら、VirtualDisplayを生成、スクリーンショットを取得するためのSurface(ImageReader)をインスタンス化します。
ImageReaderから画像イメージの読み出しはImageAvailableListenerを使用します。

public class SnapService extends Service {
    private static final String     TAG = SnapService.class.getSimpleName();
    private static final String     FILE_NAME = "temporary";
    private static final int        REQUEST_CODE = 3;
    private static final int        ID = 301;
    private Context                 context;
    private MediaProjection         mediaProjection;
    private MediaProjection.Callback
                                    callback;
    private LocalBroadcastManager   localBroadcastManager;
    private Handler                 handler = new Handler(Looper.getMainLooper());
    private Runnable                runnable;
    public static final String      START = "START";
    public static final String      STOP = "STOP";
    // インタフェース
    private interface OnClickListener {
        void onClick();
    }
    private interface OnCaptureListener {
        void onCapture(Bitmap bitmap);
    }
    :
    private class FloatingButton {
        android.view.WindowManager
                    windowManager;
        ImageView   imageView;
        :
        @SuppressLint("ClickableViewAccessibility")
        private FloatingButton(Context context, OnClickListener onClickListener) {
            DisplayMetrics displayMetrics =  context.getResources().getDisplayMetrics();
            windowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
            imageView = new ImageView(context);
            :
            imageView.setOnTouchListener(new View.OnTouchListener() {
                final int  MAX_CLICK_DURATION = 300;
                long time = 0;
                :
                @SuppressLint("ClickableViewAccessibility")
                @Override
                public boolean onTouch(View view, android.view.MotionEvent motionEvent) {
                    switch (motionEvent.getAction()) {
                        :
                        case ACTION_UP:
                            time = System.currentTimeMillis() - time;
                            if (time < MAX_CLICK_DURATION) {
                                time = System.currentTimeMillis();
                                ExecutorService executorService = Executors.newSingleThreadExecutor();
                                executorService.execute(() -> {
                                    windowManager.removeView(imageView);
                                    imageView = null;
                                    new Handler(Looper.getMainLooper()).post(onClickListener::onClick);
                                });
                            }
                    }
                    return true;
                }
            });
        }
        :
    }
    private class Capture {
        DisplayMetrics      displayMetrics;
        ImageReader         reader;
        VirtualDisplay      virtualDisplay;
        OnCaptureListener   onCaptureListener;
        private Capture() {
            displayMetrics = context.getResources().getDisplayMetrics();
            reader = ImageReader.newInstance(displayMetrics.widthPixels, displayMetrics.heightPixels, PixelFormat.RGBA_8888, 2);
            reader.setOnImageAvailableListener(imageReader -> {
                // onCapture
                if (virtualDisplay != null) {
                    if (onCaptureListener != null) {
                        onCaptureListener.onCapture(bitmap(imageReader));
                    }
                }
            }, null);
        }
        private void run(MediaProjection mediaProjection, OnCaptureListener onCaptureListener) {
            this.onCaptureListener = onCaptureListener;
            if (virtualDisplay == null) {
                virtualDisplay = mediaProjection.createVirtualDisplay("VirtualDisplay",
                        displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.densityDpi,
                        DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                        reader.getSurface(), null, null);
            }
        }
        private void stop() {
            if (reader != null) reader.close();
            if (virtualDisplay != null) virtualDisplay.release();
            virtualDisplay = null;
            onCaptureListener = null;
        }
        private Bitmap bitmap(ImageReader imageReader) {
            Bitmap bitmap = null;
            Image image = imageReader.acquireLatestImage();
            DisplayMetrics displayMetrics =  context.getResources().getDisplayMetrics();
            Image.Plane[] planes = image.getPlanes();
            if (planes.length > 0) {
                bitmap = Bitmap.createBitmap(planes[0].getRowStride() / planes[0].getPixelStride(), displayMetrics.heightPixels, Bitmap.Config.ARGB_8888);
                bitmap.copyPixelsFromBuffer(planes[0].getBuffer());
            }
            image.close();
            return bitmap;
        }
    }
    private FloatingButton  floatingButton  = null;
    private Capture         capture = null;

    @Override
    public void onCreate() {
        super.onCreate();
        context = getApplicationContext();
        // サービス通信用レシーバー
        localBroadcastManager = LocalBroadcastManager.getInstance(context);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        String action = intent.getAction();
        if (action == null || action.equals(START)) {
            PendingIntent pendingIntent1 = PendingIntent.getActivity(context, REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);
            NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationChannel channel = new NotificationChannel(TAG, TAG, NotificationManager.IMPORTANCE_DEFAULT);
            channel.setSound(null, null);
            channel.enableLights(false);
            channel.setLightColor(R.color.blue);
            channel.enableVibration(false);
            if (notificationManager != null) {
                notificationManager.createNotificationChannel(channel);
                Notification notification = new NotificationCompat.Builder(context, TAG)
                        .setContentTitle(context.getString(R.string.app_name))
                        .setContentText(TAG)
                        .setSmallIcon(R.drawable.ic_camera)
                        .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.service))
                        .setContentIntent(pendingIntent1)
                        .addAction(new NotificationCompat.Action.Builder(R.drawable.ic_round_cancel, context.getString(R.string.menu_exit),
                                PendingIntent.getBroadcast(getApplicationContext(), 0 , new Intent(context, NotificationReceiver.class).setAction(DELETE_NOTIFICATION), PendingIntent.FLAG_IMMUTABLE)).build())
                        .setDeleteIntent(PendingIntent.getBroadcast(getApplicationContext(), 0 , new Intent(context, NotificationReceiver.class).setAction(DELETE_NOTIFICATION), PendingIntent.FLAG_IMMUTABLE))
                        .build();
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    startForeground(ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION);
                } else {
                    startForeground(ID, notification);
                }
                MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) getSystemService(Service.MEDIA_PROJECTION_SERVICE);
                mediaProjection = mediaProjectionManager.getMediaProjection(Activity.RESULT_OK, intent);
                callback = new MediaProjection.Callback() {
                    @Override
                    public void onCapturedContentResize(int width, int height) {
                    }
                    @Override
                    public void onCapturedContentVisibilityChanged(boolean isVisible) {
                    }
                    @Override
                    public void onStop() {
                        super.onStop();
                    }
                };
                mediaProjection.registerCallback(callback, null);
                startOverlay();
            }
        } else {
            stopForeground(true);
            stopOverlay();
        }
        return START_NOT_STICKY;
    }
    private void startOverlay() {
        floatingButton = new FloatingButton(this, () -> {
            if (mediaProjection != null && capture != null) {
                capture.run(mediaProjection, bitmap -> {
                    // onClick
                    InternalStorageHandler internalStorageHandler = new InternalStorageHandler(context);
                    if (internalStorageHandler.initializeFile(FILE_NAME)) {
                        internalStorageHandler.writeFileBitmap(bitmap);
                    }
                    if (capture != null) capture.stop();
                    stopSelf();
                });
            }
        });
        capture = new Capture();
        floatingButton.visible(true);
        // ======================================================================
        // ALIVE送信
        // ======================================================================
        handler = new Handler(Looper.getMainLooper());
        runnable = new Runnable() {
            @Override
            public void run() {
                Intent intent = new Intent().setAction("SEND_MESSAGE");
                intent.putExtra("ALIVE", System.currentTimeMillis());
                localBroadcastManager.sendBroadcast(intent);
                handler.postDelayed(this, 1000);
            }
        };
        handler.post(runnable);
    }
    private void stopOverlay() {
        if (floatingButton != null) {
            floatingButton.visible(false);
            floatingButton = null;
        }
        capture = null;
        Intent intent = new Intent().setAction("SEND_MESSAGE");
        intent.putExtra("ALIVE", 0);
        localBroadcastManager.sendBroadcast(intent);
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (handler != null) handler.removeCallbacks(runnable);
        stopForeground(true);
        stopOverlay();
        if (mediaProjection != null) {
            mediaProjection.unregisterCallback(callback);
            mediaProjection = null;
        }
        ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.AppTask> appTasks = activityManager.getAppTasks();
        if (!appTasks.isEmpty()) {
            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
                activityManager.moveTaskToFront(appTasks.get(0).getTaskInfo().taskId, 0);
            } else {
                activityManager.moveTaskToFront(appTasks.get(0).getTaskInfo().id, 0);
            }
        }
    }
}

インタフェース(OnClickListener)でフローティングアイコン(ImageView)のダブルタップをフックします。
ダブルタップは300ミリ秒以内に2回のタップで判定しています。
スクリーンショットを取得の際はフローティングアイコンが映り込まないように非表示にしてします。
また、非表示が先に実行されるようにシングルスレッドで実行します。
スクリーンショットは画像ファイル(temporary)に出力し、Activityに連携しています。
InternalStorageHandlerは内部ストレージに画像ファイルを出力するクラスです。
Service起動中はLocalBroadcastManagerActivityにALIVE通信を一定間隔で送信します。

取得したスクリーンショットを表示

今回は、ここまでです。

実行中の他のアプリ画面のスクリーンショットを取得しているAndroidアプリです。

誤字脱字、意味不明でわかりづらい、
もっと詳しく知りたいなどのご意見は、
このページの最後にある
コメントか、
こちらから、お願いいたします♪

ポチッとして頂けると、
次のコンテンツを作成する励みになります♪

ブログランキング・にほんブログ村へ

これからAndroidのアプリ開発やJavaでの開発を始めたい方へ

アプリケーション開発経験がない方や、アプリケーション開発経験がある方でも、Java や C# などのオブジェクト指向言語が初めての方は、Android のアプリ開発ができるようになるには、かなりの時間がかかります。
オンラインスクールでの習得を、強くおススメします。

未経験者からシステムエンジニアを目指すのに最適です。まずは無料相談から♪

未経験者からプログラマーを目指すのに最適です。まずは無料カウンセリングから♪

カリキュラムとサポートがしっかりしています。お得なキャンペーンとかいろいろやっています♪

ゲーム系に強いスクール、UnityやUnrealEngineを習得するのに最適です。まずは無料オンライン相談から♪

参考になったら、💛をポッチとしてね♪

スポンサーリンク
msakiをフォローする
スポンサーリンク

コメント欄

タイトルとURLをコピーしました