Skip to content

Commit 16323e8

Browse files
committed
feat: APKインストール機能をMethodChannelを通じて実装し、エラーハンドリングを強化
1 parent 83c817b commit 16323e8

File tree

2 files changed

+278
-65
lines changed

2 files changed

+278
-65
lines changed
Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,214 @@
11
package com.tsukuba.atcoder.shojin
22

3+
import android.app.PendingIntent
4+
import android.content.BroadcastReceiver
5+
import android.content.Context
6+
import android.content.Intent
7+
import android.content.IntentFilter
8+
import android.content.pm.PackageInstaller
9+
import android.net.Uri
10+
import android.os.Build
11+
import androidx.core.content.FileProvider
312
import io.flutter.embedding.android.FlutterActivity
13+
import io.flutter.embedding.engine.FlutterEngine
14+
import io.flutter.plugin.common.MethodChannel
15+
import java.io.File
416

5-
class MainActivity : FlutterActivity()
17+
class MainActivity: FlutterActivity() {
18+
private val CHANNEL = "com.example.shojin_app/patcher"
19+
private var installResultPendingIntent: PendingIntent? = null
20+
private var installResultChannel: MethodChannel.Result? = null
21+
22+
companion object {
23+
private const val ACTION_INSTALL_COMPLETE = "com.example.shojin_app.INSTALL_COMPLETE"
24+
private const val EXTRA_STATUS = "EXTRA_STATUS"
25+
private const val REQUEST_CODE_INSTALL = 123
26+
}
27+
28+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
29+
super.configureFlutterEngine(flutterEngine)
30+
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
31+
if (call.method == "installApk") {
32+
val apkPath = call.argument<String>("apkPath")
33+
if (apkPath != null) {
34+
installResultChannel = result
35+
installApk(apkPath)
36+
} else {
37+
result.error("INVALID_ARGUMENT", "apkPath is null", null)
38+
}
39+
} else {
40+
result.notImplemented()
41+
}
42+
}
43+
}
44+
45+
private fun installApk(apkPath: String) {
46+
try {
47+
val file = File(apkPath)
48+
if (!file.exists()) {
49+
installResultChannel?.error("FILE_NOT_FOUND", "APK file not found at $apkPath", null)
50+
return
51+
}
52+
53+
val packageInstaller = applicationContext.packageManager.packageInstaller
54+
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
55+
val sessionId = packageInstaller.createSession(params)
56+
val session = packageInstaller.openSession(sessionId)
57+
58+
val outputStream = session.openWrite("shojin_app_update", 0, file.length())
59+
val inputStream = file.inputStream()
60+
inputStream.copyTo(outputStream)
61+
session.fsync(outputStream)
62+
outputStream.close()
63+
inputStream.close()
64+
65+
val intent = Intent(applicationContext, InstallReceiver::class.java)
66+
intent.action = ACTION_INSTALL_COMPLETE
67+
// Add sessionId to intent extras if needed for more complex scenarios,
68+
// but for basic status, it might not be strictly necessary if we only handle one install at a time.
69+
70+
installResultPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
71+
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE_INSTALL, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
72+
} else {
73+
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE_INSTALL, intent, PendingIntent.FLAG_UPDATE_CURRENT)
74+
}
75+
76+
session.commit(installResultPendingIntent!!.intentSender)
77+
session.close()
78+
// The result will be sent via BroadcastReceiver
79+
} catch (e: Exception) {
80+
installResultChannel?.error("INSTALL_FAILED", "Failed to install APK: ${e.message}", e.toString())
81+
}
82+
}
83+
84+
private val installBroadcastReceiver = object : BroadcastReceiver() {
85+
override fun onReceive(context: Context, intent: Intent) {
86+
if (intent.action == ACTION_INSTALL_COMPLETE) {
87+
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
88+
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE) ?: "No message"
89+
90+
val resultData = mutableMapOf<String, Any>()
91+
92+
when (status) {
93+
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
94+
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
95+
if (confirmationIntent != null) {
96+
// This intent should be started by the system automatically.
97+
// If not, we might need to start it here, but typically the system handles this.
98+
// For now, we assume the system handles it. If issues arise, we might need to start it:
99+
// context.startActivity(confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
100+
// However, this part is tricky as the receiver might not be the right place to start an activity
101+
// that requires user interaction without careful handling.
102+
// The primary purpose here is to get the final status.
103+
// The user action itself is handled by the system dialog.
104+
// We don't send a result back to Flutter yet, as the action is pending.
105+
return
106+
} else {
107+
resultData["status"] = PackageInstaller.STATUS_FAILURE // Or a custom code
108+
resultData["message"] = "Confirmation intent was null, but user action is pending."
109+
installResultChannel?.success(resultData)
110+
}
111+
}
112+
PackageInstaller.STATUS_SUCCESS -> {
113+
resultData["status"] = 0 // Success
114+
resultData["message"] = "Installation successful"
115+
installResultChannel?.success(resultData)
116+
}
117+
PackageInstaller.STATUS_FAILURE -> {
118+
resultData["status"] = 1 // Generic failure
119+
resultData["message"] = "Installation failed: $message"
120+
installResultChannel?.error("INSTALL_FAILURE_DEVICE", message, null)
121+
}
122+
PackageInstaller.STATUS_FAILURE_ABORTED -> {
123+
resultData["status"] = 3 // User cancelled
124+
resultData["message"] = "Installation cancelled by user: $message"
125+
installResultChannel?.success(resultData) // Or error, depending on how Flutter expects it
126+
}
127+
PackageInstaller.STATUS_FAILURE_BLOCKED -> {
128+
resultData["status"] = 2 // Blocked
129+
resultData["message"] = "Installation blocked: $message"
130+
installResultChannel?.error("INSTALL_BLOCKED", message, null)
131+
}
132+
PackageInstaller.STATUS_FAILURE_CONFLICT -> {
133+
resultData["status"] = 4 // Conflict
134+
resultData["message"] = "Installation conflict: $message"
135+
installResultChannel?.error("INSTALL_CONFLICT", message, null)
136+
}
137+
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> {
138+
resultData["status"] = 5 // Incompatible
139+
resultData["message"] = "Installation incompatible: $message"
140+
installResultChannel?.error("INSTALL_INCOMPATIBLE", message, null)
141+
}
142+
PackageInstaller.STATUS_FAILURE_INVALID -> {
143+
resultData["status"] = 6 // Invalid APK
144+
resultData["message"] = "Installation invalid APK: $message"
145+
installResultChannel?.error("INSTALL_INVALID_APK", message, null)
146+
}
147+
PackageInstaller.STATUS_FAILURE_STORAGE -> {
148+
resultData["status"] = 7 // Storage issue
149+
resultData["message"] = "Installation storage issue: $message"
150+
installResultChannel?.error("INSTALL_STORAGE_ISSUE", message, null)
151+
}
152+
else -> {
153+
resultData["status"] = status // Unknown status
154+
resultData["message"] = "Installation unknown status $status: $message"
155+
installResultChannel?.error("INSTALL_UNKNOWN_STATUS", message, status.toString())
156+
}
157+
}
158+
// Clean up
159+
installResultChannel = null
160+
}
161+
}
162+
}
163+
164+
override fun onResume() {
165+
super.onResume()
166+
val filter = IntentFilter(ACTION_INSTALL_COMPLETE)
167+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
168+
registerReceiver(installBroadcastReceiver, filter, RECEIVER_EXPORTED)
169+
} else {
170+
registerReceiver(installBroadcastReceiver, filter)
171+
}
172+
}
173+
174+
override fun onPause() {
175+
super.onPause()
176+
unregisterReceiver(installBroadcastReceiver)
177+
}
178+
}
179+
180+
// Create a new file for the BroadcastReceiver: InstallReceiver.kt
181+
// (This is just a placeholder, the actual receiver logic is now inside MainActivity for simplicity,
182+
// but it's good practice to have it in a separate file if it grows complex or is used by other components)
183+
// package com.example.shojin_app
184+
//
185+
// import android.content.BroadcastReceiver
186+
// import android.content.Context
187+
// import android.content.Intent
188+
// import android.content.pm.PackageInstaller
189+
// import android.util.Log
190+
//
191+
// class InstallReceiver : BroadcastReceiver() {
192+
// override fun onReceive(context: Context, intent: Intent) {
193+
// // This receiver is now defined and registered within MainActivity.
194+
// // If you want to keep it separate, you'd need to ensure MainActivity can access the result.
195+
// // For this implementation, we've integrated it into MainActivity.
196+
// // The intent that triggers this receiver is created in MainActivity's installApk method.
197+
// // It should carry the status of the installation.
198+
//
199+
// // Example of how it would look if it were separate and needed to send data back,
200+
// // perhaps via another broadcast or by updating a shared state.
201+
// // However, for direct MethodChannel communication, integrating it or having a clear callback mechanism
202+
// // to MainActivity is simpler.
203+
//
204+
// // val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE)
205+
// // Log.d("InstallReceiver", "Installation status: $status")
206+
// // val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
207+
// // Log.d("InstallReceiver", "Installation message: $message")
208+
209+
// // If this receiver were truly separate and needed to communicate back to Flutter,
210+
// // it would be more complex. It might need to start MainActivity with specific extras,
211+
// // or use a service, or write to SharedPreferences that Flutter then reads.
212+
// // The current integrated approach in MainActivity is more direct for this use case.
213+
// }
214+
// }

0 commit comments

Comments
 (0)