アプリでスクリーンショットして画像保存

Uncategorized
1k words

アプリでアンドロイド端末のスクリーンショットを撮影して、画像保存するまでを紹介します。

よくあるサンプルだと、アプリを開いてるときのみスクリーンショットしているため、アプリを閉じるとスクリーンショットできないのがほとんどでした。

今回紹介するアプリは、フォアグラウンドサービスでスクリーンショットを撮影するので、アプリを閉じてもスクリーンショットしてくれます。

あと、最新のSDKバージョン (API30 (Android 11 (R))) に対応したソースで書いてあります。

実行するとこのようにアプリを閉じてもスクリーンショットが保存されていきます。

保存されたスクリーンショットの確認

環境

  • Windows 11 Home 21H2
  • Android Studio Bumblebee | 2021.1.1 Patch 1
  • API 30、Android 11 (R)

手順

プロジェクト作成

今回は Empty Activity を使います。

プロジェクトの作成

設定は下記の通りです。

  • Language: Java
  • Minimum SDK: API 30、Android 11 (R)

プロジェクトの設定

ソース

完成品は こちら に置いておきます。

activity_main.xml

画面を作ります。

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
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<EditText
android:id="@+id/editTextNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:ems="10"
android:inputType="number"
android:text="10"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="28dp"
android:text="回"
app:layout_constraintStart_toEndOf="@+id/editTextNumber"
app:layout_constraintTop_toTopOf="parent" />

<EditText
android:id="@+id/editTextNumber2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="4dp"
android:ems="10"
android:inputType="number"
android:text="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextNumber" />

<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="32dp"
android:text="秒毎"
app:layout_constraintStart_toEndOf="@+id/editTextNumber2"
app:layout_constraintTop_toBottomOf="@+id/textView" />

<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="8dp"
android:text="実行"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/editTextNumber2"
android:onClick="screenshot"/>
</androidx.constraintlayout.widget.ConstraintLayout>

画面の作成

MainActivity.java

実行ボタンを押したとき、フォアグラウンドで動く CaptureService を作りサービス実行させます。

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
public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

public void screenshot(View view) {
MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) this.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
activityResultLauncher.launch(mediaProjectionManager.createScreenCaptureIntent());
}

ActivityResultLauncher<Intent> activityResultLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {

EditText editText1 = findViewById(R.id.editTextNumber);
int maxCount = Integer.parseInt(editText1.getText().toString());
EditText editText2 = findViewById(R.id.editTextNumber2);
int waitSeconds = Integer.parseInt(editText2.getText().toString());

Intent intent = new Intent(getApplication(), CaptureService.class);
intent.putExtra("ResultCode", result.getResultCode());
intent.putExtra("ResultData", result.getData());
intent.putExtra("MaxCount", maxCount);
intent.putExtra("WaitSeconds", waitSeconds);
startForegroundService(intent);
}
});
}

CaptureService.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
public class CaptureService extends Service {

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

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();

NotificationChannel channel = new NotificationChannel("channelId", "channelName", NotificationManager.IMPORTANCE_DEFAULT);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);

Notification notification = new Notification.Builder(this, "channelId")
.setContentTitle("notification_title")
.setContentText("notification_message")
.build();
startForeground(1, notification);

DisplayMetrics metrics = getResources().getDisplayMetrics();
int width = metrics.widthPixels;
int height = metrics.heightPixels;
int density = metrics.densityDpi;

int resultCode = intent.getIntExtra("ResultCode", 0);
Intent resultData = intent.getParcelableExtra("ResultData");
int maxCount = intent.getIntExtra("MaxCount", 0);
int waitSeconds = intent.getIntExtra("WaitSeconds", 0);

MediaProjectionManager mediaProjectionManager = (MediaProjectionManager) this.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
MediaProjection mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, resultData);
ImageReader imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 2);
VirtualDisplay virtualDisplay = mediaProjection.createVirtualDisplay("ScreenCapture", width, height, density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(), null, null);

Thread thread = new Thread(() -> {
try {
for (int i = 0; i < maxCount; i++) {
Image image = imageReader.acquireLatestImage();
// 画面に変更がない場合、NULL
if (image != null) {

// Image -> Bitmap
Image.Plane[] planes = image.getPlanes();
ByteBuffer buffer = planes[0].getBuffer();
int pixelStride = planes[0].getPixelStride();
int rowStride = planes[0].getRowStride();
int rowPadding = rowStride - pixelStride * width;
Bitmap bitmap = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888);
bitmap.copyPixelsFromBuffer(buffer);
image.close();

File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).getPath() + "/image_" + System.currentTimeMillis() + ".png");
FileOutputStream outStream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outStream);
outStream.close();
}

Log.d("CaptureService", "onStartCommand: " + i);
Thread.sleep(waitSeconds * 1000);
}
} catch (Exception e) {
Log.e("CaptureService", e.getMessage());
}

stopSelf();
});
thread.start();

return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show();
}
}

AndroidManifest.xml

マニフェストにパーミッションとサービスを宣言します。

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
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapplication">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication">
<service android:name=".CaptureService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

実行

実行するとピクチャフォルダにスクリーンショットが保存されていきます。

ピクチャフォルダにスクリーンショットが保存される

確認

確認のしかたは次の通りです。

Filesアプリを開き、

Filesアプリを開く

左メニューからアンドロイド端末を選択し、

左メニューからアンドロイド端末の選択

Picturesフォルダーを選択し、

Picturesフォルダーの選択

保存されたスクリーンショットが確認できます。

保存されたスクリーンショットの確認

参照