Skip to content

Commit 9a9a519

Browse files
committed
WIP
1 parent 4594494 commit 9a9a519

File tree

3 files changed

+60
-51
lines changed

3 files changed

+60
-51
lines changed

android/src/main/java/sk/fourq/otaupdate/OtaUpdatePlugin.java

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import android.os.Looper;
1616
import android.os.Message;
1717
import android.util.Log;
18+
import androidx.annotation.RequiresApi;
1819
import androidx.core.app.ActivityCompat;
1920
import androidx.core.content.ContextCompat;
2021
import androidx.core.content.FileProvider;
@@ -49,11 +50,12 @@
4950
import java.util.Arrays;
5051
import java.util.Iterator;
5152
import java.util.Map;
53+
import java.util.Objects;
5254

5355
/**
5456
* OtaUpdatePlugin
5557
*/
56-
@TargetApi(Build.VERSION_CODES.M)
58+
@RequiresApi(Build.VERSION_CODES.M)
5759
public class OtaUpdatePlugin implements FlutterPlugin, ActivityAware, EventChannel.StreamHandler, MethodCallHandler, PluginRegistry.RequestPermissionsResultListener, ProgressListener {
5860

5961
//CONSTANTS
@@ -124,7 +126,7 @@ public void onDetachedFromActivity() {
124126
//METHOD LISTENER
125127
@Override
126128
public void onMethodCall(MethodCall call, Result result) {
127-
Log.d(TAG, "onMethodCall "+call.method);
129+
Log.d(TAG, "onMethodCall " + call.method);
128130
if (call.method.equals("getAbi")) {
129131
result.success(Build.SUPPORTED_ABIS[0]);
130132
} else if (call.method.equals("cancel")) {
@@ -148,31 +150,28 @@ public void onListen(Object arguments, EventChannel.EventSink events) {
148150
Log.d(TAG, "STREAM OPENED");
149151
progressSink = events;
150152
//READ ARGUMENTS FROM CALL
151-
Map argumentsMap = ((Map) arguments);
152-
downloadUrl = argumentsMap.get(ARG_URL).toString();
153+
//noinspection unchecked
154+
Map<String, String> argumentsMap = ((Map<String, String>) arguments);
155+
downloadUrl = argumentsMap.get(ARG_URL);
153156
try {
154-
String headersJson = argumentsMap.get(ARG_HEADERS).toString();
155-
if (!headersJson.isEmpty()) {
157+
String headersJson = argumentsMap.get(ARG_HEADERS);
158+
if (headersJson != null && !headersJson.isEmpty()) {
156159
headers = new JSONObject(headersJson);
157160
}
158161
} catch (JSONException e) {
159162
Log.e(TAG, "ERROR: " + e.getMessage(), e);
160163
}
161164
if (argumentsMap.containsKey(ARG_FILENAME) && argumentsMap.get(ARG_FILENAME) != null) {
162-
filename = argumentsMap.get(ARG_FILENAME).toString();
165+
filename = argumentsMap.get(ARG_FILENAME);
163166
} else {
164167
filename = DEFAULT_APK_NAME;
165168
}
166169
if (argumentsMap.containsKey(ARG_CHECKSUM) && argumentsMap.get(ARG_CHECKSUM) != null) {
167-
checksum = argumentsMap.get(ARG_CHECKSUM).toString();
170+
checksum = argumentsMap.get(ARG_CHECKSUM);
168171
}
169172
// user-provided provider authority
170-
Object authority = ((Map) arguments).get(ARG_ANDROID_PROVIDER_AUTHORITY);
171-
if (authority != null) {
172-
androidProviderAuthority = authority.toString();
173-
} else {
174-
androidProviderAuthority = context.getPackageName() + "." + "ota_update_provider";
175-
}
173+
String authority = argumentsMap.get(ARG_ANDROID_PROVIDER_AUTHORITY);
174+
androidProviderAuthority = Objects.requireNonNullElseGet(authority, () -> context.getPackageName() + "." + "ota_update_provider");
176175
executeDownload();
177176
}
178177

@@ -222,7 +221,7 @@ private void executeDownload() {
222221
if (!file.delete()) {
223222
Log.e(TAG, "WARNING: unable to delete old apk file before starting OTA");
224223
}
225-
} else if (!file.getParentFile().exists()) {
224+
} else if (file.getParentFile() != null && !file.getParentFile().exists()) {
226225
if (!file.getParentFile().mkdirs()) {
227226
reportError(OtaStatus.INTERNAL_ERROR, "unable to create ota_update folder in internal storage", null);
228227
}
@@ -255,7 +254,9 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
255254
}
256255
try {
257256
BufferedSink sink = Okio.buffer(Okio.sink(file));
258-
sink.writeAll(response.body().source());
257+
if (response.body() != null) {
258+
sink.writeAll(response.body().source());
259+
}
259260
sink.close();
260261
} catch (StreamResetException ex) {
261262
// Thrown when the call was canceled using 'cancel()'
@@ -278,7 +279,7 @@ public void onResponse(@NotNull Call call, @NotNull Response response) throws IO
278279

279280
/**
280281
* Download has been completed
281-
*
282+
* <p>
282283
* 1. Check if file exists
283284
* 2. If checksum was provided, compute downloaded file checksum and compare with provided value
284285
* 3. If checks above pass, trigger installation
@@ -293,9 +294,8 @@ private void onDownloadComplete(final String destination, final Uri fileUri) {
293294
reportError(OtaStatus.DOWNLOAD_ERROR, "File was not downloaded", null);
294295
return;
295296
}
296-
297297
if (checksum != null) {
298-
//IF user provided checksum verify file integrity
298+
//IF the user provided checksum verify file integrity
299299
try {
300300
if (!Sha256ChecksumValidator.validateChecksum(checksum, downloadedFile)) {
301301
//SEND CHECKSUM ERROR EVENT
@@ -309,23 +309,19 @@ private void onDownloadComplete(final String destination, final Uri fileUri) {
309309
}
310310
}
311311
//TRIGGER APK INSTALLATION
312-
handler.post(new Runnable() {
313-
@Override
314-
public void run() {
315-
executeInstallation(fileUri, downloadedFile);
316-
}
317-
}
318-
312+
handler.post(() -> executeInstallation(fileUri, downloadedFile)
319313
);
320314
}
321315

322-
/**
316+
/**
323317
* Check if app has INSTALL_PACKAGES permission (system app privilege)
324318
*/
325319
private boolean hasInstallPackagesPermission() {
326320
try {
327-
return context.checkCallingOrSelfPermission("android.permission.INSTALL_PACKAGES")
321+
boolean hasInstallPackages = context.checkCallingOrSelfPermission("android.permission.INSTALL_PACKAGES")
328322
== PackageManager.PERMISSION_GRANTED;
323+
Log.d(TAG, "INSTALL_PACKAGES permission: " + hasInstallPackages);
324+
return hasInstallPackages;
329325
} catch (Exception e) {
330326
Log.w(TAG, "Error checking INSTALL_PACKAGES permission", e);
331327
return false;
@@ -338,39 +334,47 @@ private boolean hasInstallPackagesPermission() {
338334
private void executeSilentInstallation(File downloadedFile) {
339335
try {
340336
Log.d(TAG, "Attempting silent installation for system app");
341-
342337
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
338+
// Configure session parameters.
339+
// MODE_FULL_INSTALL means we’re doing a full APK installation (not a staged/delta update).
343340
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
344-
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
345-
341+
PackageInstaller.SessionParams.MODE_FULL_INSTALL
342+
);
343+
// Create a new installation session and get its unique ID
344+
// Open the session so we can write the APK bytes into it
346345
int sessionId = packageInstaller.createSession(params);
347346
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
348-
349347
try (OutputStream out = session.openWrite("package", 0, -1);
350-
InputStream in = new FileInputStream(downloadedFile)) {
348+
InputStream in = new FileInputStream(downloadedFile)
349+
) {
350+
// Buffer for copying data from the APK file into the session
351351
byte[] buffer = new byte[65536];
352352
int c;
353353
while ((c = in.read(buffer)) != -1) {
354354
out.write(buffer, 0, c);
355355
}
356356
session.fsync(out);
357357
}
358-
359-
// Create an intent for the installation result
358+
359+
// Create intent for the installation result
360360
Intent intent = new Intent(context, OtaUpdatePlugin.class);
361+
// Wrap the result Intent in a PendingIntent, which gives us an IntentSender for commit().
362+
// On Android 12 (S) and above, PendingIntent must be declared mutable/immutable explicitly.
361363
PendingIntent pendingIntent = PendingIntent.getBroadcast(
362364
context,
363365
sessionId,
364366
intent,
365-
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
366-
? PendingIntent.FLAG_MUTABLE
367-
: PendingIntent.FLAG_UPDATE_CURRENT);
368-
367+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
368+
? PendingIntent.FLAG_MUTABLE
369+
: PendingIntent.FLAG_UPDATE_CURRENT);
370+
371+
// Commit the session. This hands control over to the system to actually perform the install.
372+
// The provided IntentSender will be invoked with the result of the installation.
369373
session.commit(pendingIntent.getIntentSender());
370374
session.close();
371-
375+
372376
Log.d(TAG, "Silent installation session committed");
373-
377+
// NOTIFY DART PART OF THE PLUGIN, THAT INSTALLATION HAS BEEN SUCCESSFUL
374378
if (progressSink != null) {
375379
progressSink.success(Arrays.asList("" + OtaStatus.INSTALLING.ordinal(), ""));
376380
progressSink.endOfStream();
@@ -381,9 +385,10 @@ private void executeSilentInstallation(File downloadedFile) {
381385
reportError(OtaStatus.INTERNAL_ERROR, "Silent installation failed: " + e.getMessage(), e);
382386
}
383387
}
388+
384389
/**
385390
* Execute installation
386-
*
391+
* <p>
387392
* If app has INSTALL_PACKAGES permission, use silent installation
388393
* For android API level >= 24 start intent for ACTION_INSTALL_PACKAGE (native installer)
389394
* For android API level < 24 start intent ACTION_VIEW (open file, android should prompt for installation)
@@ -392,20 +397,23 @@ private void executeSilentInstallation(File downloadedFile) {
392397
* @param downloadedFile Downloaded file
393398
*/
394399
private void executeInstallation(Uri fileUri, File downloadedFile) {
395-
// Try silent installation for system apps first
400+
// Try silent installation for system apps first
396401
if (hasInstallPackagesPermission()) {
397402
Log.d(TAG, "System app detected, using silent installation");
398403
executeSilentInstallation(downloadedFile);
399404
return;
400405
}
401406
Intent intent;
402-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
403-
//AUTHORITY NEEDS TO BE THE SAME ALSO IN MANIFEST
404-
Uri apkUri = FileProvider.getUriForFile(context, androidProviderAuthority, downloadedFile);
405-
intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
406-
intent.setData(apkUri);
407-
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
408-
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
407+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
408+
Log.d(TAG, "System app detected, using silent installation");
409+
executeSilentInstallation(downloadedFile);
410+
// //AUTHORITY NEEDS TO BE THE SAME ALSO IN MANIFEST
411+
// Uri apkUri = FileProvider.getUriForFile(context, androidProviderAuthority, downloadedFile);
412+
// intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
413+
// intent.setData(apkUri);
414+
// intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
415+
// .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
416+
return;
409417
} else {
410418
intent = new Intent(Intent.ACTION_VIEW);
411419
intent.setDataAndType(fileUri, "application/vnd.android.package-archive");

example/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
to allow setting breakpoints, to provide hot reload, etc.
66
-->
77
<uses-permission android:name="android.permission.INTERNET"/>
8+
<uses-permission android:name="android.permission.INSTAL_PACKAGES"/>
89

910

1011
<application

example/android/settings.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pluginManagement {
1919
plugins {
2020
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
2121
id "com.android.application" version '8.7.3' apply false
22-
id "org.jetbrains.kotlin.android" version "2.0.21" apply false
22+
id "org.jetbrains.kotlin.android" version "2.1.0" apply false
2323
}
2424

2525
include ":app"

0 commit comments

Comments
 (0)