ともちゃんのアプリ開発日記

組込みC言語プログラマだったともちゃんが、四苦八苦しながら、AndroidのJAVA/Kotlin、iOSのSwiftUIを習得して行きます。ともちゃんの備忘録も兼ねています。

画像の自動リネーム

DSC auto renameに触発されて、どうやったらこのようなアプリが作れるのかと思い、PM Rename(Photo/Movie Rename)を作りました。

1. AndroidManifest.xml

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

リネームサービスを起動するので、
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
が必要です。
また、ストレージのファイル名を変更するので、
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
が必要です。

2.  MainSerivce

写真が撮られたことの監視は、Content Ovserverを使います。そのContent Observerの登録です。

fun regContentObserver(context: Context) {
val uriPhotoIn = MediaStore.Images.Media.INTERNAL_CONTENT_URI
val uriPhotoEx = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val uriMovieIn = MediaStore.Video.Media.INTERNAL_CONTENT_URI
val uriMovieEx = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
contentObserverPhotoIn = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmPhotoIn(context, delay)
}
}
contentObserverPhotoEx = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmPhotoEx(context, delay)
}
}
contentObserverMovieIn = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmMovieIn(context, delay)
}
}
contentObserverMovieEx = object : ContentObserver(Handler()) {
override fun onChange(selfChange: Boolean) {
super.onChange(selfChange)
val prefs = context.getSharedPreferences("Setting", Context.MODE_PRIVATE)
val delay = prefs.getInt("delay", 0)
registerAlarmMovieEx(context, delay)
}
}
context.contentResolver.registerContentObserver(uriPhotoIn, true, contentObserverPhotoIn)
context.contentResolver.registerContentObserver(uriPhotoEx, true, contentObserverPhotoEx)
context.contentResolver.registerContentObserver(uriMovieIn, true, contentObserverMovieIn)
context.contentResolver.registerContentObserver(uriMovieEx, true, contentObserverMovieEx)
}

これを、Main Serviceから呼びます。

override fun onCreate() {
super.onCreate()

@TargetApi(Build.VERSION_CODES.O)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(MAIN_CHANNEL, getString(R.string.main_channel), NotificationManager.IMPORTANCE_LOW)
/** 指定できる値は以下7種類。
NotificationManager.IMPORTANCE_UNSPECIFIED (-1000);
NotificationManager.IMPORTANCE_NONE (0);
NotificationManager.IMPORTANCE_MIN (1);
NotificationManager.IMPORTANCE_LOW (2);
NotificationManager.IMPORTANCE_DEFAULT (3);
NotificationManager.IMPORTANCE_HIGH (4);
NotificationManager.IMPORTANCE_MAX (5);
**/
notificationManager.createNotificationChannel(channel)

}

val intent = Intent(applicationContext, MainActivity::class.java)
val intents = arrayOf(intent)
val pendingIntent = PendingIntent.getActivities(applicationContext, 0, intents, PendingIntent.FLAG_UPDATE_CURRENT)
val builder = NotificationCompat.Builder(applicationContext, MAIN_CHANNEL)
builder.setContentIntent(pendingIntent)
builder.setContentTitle(resources.getString(R.string.app_name))
builder.setContentText(resources.getString(R.string.please_tap_here))
builder.setWhen(System.currentTimeMillis())
builder.setAutoCancel(false)
builder.setSmallIcon(R.drawable.notification_main_small)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.notification_main_large))

val notification = builder.build()
notification.flags = Notification.FLAG_NO_CLEAR

startForeground(NOTIFICATION_ID, notification)

val commonFunc = CommonFunc()
commonFunc.regContentObserver(applicationContext)
}

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
return START_STICKY
}

override fun onDestroy() {
super.onDestroy()
val commonFunc = CommonFunc()
commonFunc.remContentObserver(applicationContext)
}

 

 Main Service自体は、Content Observerを登録すること以外何もしません。ただ、Main Serviceが動いていないのと、Content Observerが動かないので、Content Observerを動かすためだけにプカプカ浮いています。

3. アラーム

Content Observerは、アラームを起動します。

例えば、regContentObserver()内の

registerAlarmPhotoIn(context, delay)

です。

AlarmRecceiverは、RenameIntentService()を呼び出します。

class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val intentAutoRename = Intent(context, RenameIntentService::class.java)
val commonFunc = CommonFunc()
intentAutoRename.putExtra("Photo_Movie_Selection", intent?.getIntExtra("Photo_Movie_Selection", commonFunc.SelectionPhoto))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context!!.startForegroundService(intentAutoRename);
} else {
context!!.startService(intentAutoRename);
}
}
}

 4. RenameIntentService

RenameIntentService内では、

private fun renameProc(oldFile: File, newFile: File, internal_SD: Int) {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
lateinit var strTreeUri : String
when (internal_SD) {
INTERNAL_STORAGE -> strTreeUri = "treeUriIn"
SD_CARD -> strTreeUri = "treeUriEx"
}
if (prefs.getString(strTreeUri, null) != null) {
val treeUri = Uri.parse(prefs.getString(strTreeUri, null))
if (treeUri != null) {
if (!oldFile.equals(newFile)) {
val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri)
if (pickedDir != null) {
if (oldFile.parent.contains(pickedDir.name as CharSequence)) {
for (documentFile in pickedDir.listFiles()) {
if (documentFile != null) {
if ((documentFile.name.equals(oldFile.name)) && !(documentFile.name.equals(newFile.name))) {
documentFile.renameTo(newFile.name)
scanMedia(applicationContext, oldFile, newFile)
}
}
}
}
}
}
}
}
}

 という処理があり、リネームしています。

ここで、scanMedia()は、

private fun scanMedia(context: Context, oldFile: File, newFile: File) {
MediaScannerConnection.scanFile(context, arrayOf(oldFile.absolutePath, newFile.absolutePath), null, null)
}

 となっており、旧ファイル名と新ファイル名をScanしています。ミソは、旧ファイル名もScanしている点。これをしないと、Nexsu5(Android 6)やNexus 6(Android 7)でGoogleフォトが正常に画像を認識してくれません。Googleフォトのバックアップが完了するとサムネイル画像がグレーアウトしてしまいます。

5. MainActivity.kt

treeUriは、

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_INTERNAL_STORAGE_ACCESS)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, REQUEST_SD_CARD_ACCESS)
}

public override fun onActivityResult(requestCode: Int, resultCode: Int, resultData: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_INTERNAL_STORAGE_ACCESS -> {
// IntentからtreeのURIを取得します。
val treeUri = resultData!!.data

val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri!!)
var isExistDirectory = false
for (documentFile in pickedDir!!.listFiles()) {
if (documentFile.isDirectory) isExistDirectory = true
}
if (isExistDirectory) {
Toast.makeText(applicationContext, R.string.select_LEAF_folder, Toast.LENGTH_SHORT).show()
val switch = findViewById<Switch>(R.id.switchInternal)
switch.isChecked = false
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putBoolean("internal_storage_access", false)
editor.putString("treeUriIn",null)
editor.apply()
} else {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("treeUriIn", treeUri.toString())
editor.apply()
// 恒常的にPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
applicationContext.getContentResolver().takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
REQUEST_SD_CARD_ACCESS -> {
// IntentからtreeのURIを取得します。
val treeUri = resultData!!.data

val pickedDir = DocumentFile.fromTreeUri(applicationContext, treeUri!!)
var isExistDirectory = false
for (documentFile in pickedDir!!.listFiles()) {
if (documentFile.isDirectory) isExistDirectory = true
}
if (isExistDirectory) {
Toast.makeText(applicationContext, R.string.select_LEAF_folder, Toast.LENGTH_SHORT).show()
val switch = findViewById<Switch>(R.id.switchSD)
switch.isChecked = false
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putBoolean("SDaccess", false)
editor.putString("treeUriEx",null)
editor.apply()
} else {
val prefs = getSharedPreferences("Setting", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("treeUriEx", treeUri.toString())
editor.apply()
// 恒常的にPermissionを取得するにはContentResolverでtakePersistableUriPermissionを呼び出します。
applicationContext.getContentResolver().takePersistableUriPermission(
treeUri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
}
}
}
}

 

のように取得します。File.renameTo()を使わない理由は、SDカードのファイル名もリネームするためです。