react-native-update 10.52.3 → 10.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -125,10 +125,10 @@ android {
125
125
  }
126
126
  }
127
127
  }
128
- compileSdkVersion safeExtGet('compileSdkVersion', 28)
129
- buildToolsVersion safeExtGet('buildToolsVersion', '28.0.3')
128
+ compileSdkVersion safeExtGet('compileSdkVersion', 31)
129
+ buildToolsVersion safeExtGet('buildToolsVersion', '31.0.0')
130
130
  defaultConfig {
131
- minSdkVersion safeExtGet('minSdkVersion', 16)
131
+ minSdkVersion safeExtGet('minSdkVersion', 21)
132
132
  targetSdkVersion safeExtGet('targetSdkVersion', 27)
133
133
  versionCode 1
134
134
  versionName "1.81.4"
@@ -2,14 +2,8 @@
2
2
  package="cn.reactnative.modules.update">
3
3
  <application>
4
4
  <meta-data android:name="pushy_build_time" android:value="@string/pushy_build_time" />
5
- <provider
6
- android:name=".PushyFileProvider"
7
- android:authorities="${applicationId}.pushy.fileprovider"
8
- android:exported="false"
9
- android:grantUriPermissions="true">
10
- <meta-data
11
- android:name="android.support.FILE_PROVIDER_PATHS"
12
- android:resource="@xml/pushy_file_paths" />
13
- </provider>
5
+ <receiver
6
+ android:name="cn.reactnative.modules.update.PackageInstallerStatusReceiver"
7
+ android:exported="false" />
14
8
  </application>
15
9
  </manifest>
@@ -1,14 +1,8 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
2
  <application>
3
3
  <meta-data android:name="pushy_build_time" android:value="@string/pushy_build_time" />
4
- <provider
5
- android:name=".PushyFileProvider"
6
- android:authorities="${applicationId}.pushy.fileprovider"
7
- android:exported="false"
8
- android:grantUriPermissions="true">
9
- <meta-data
10
- android:name="android.support.FILE_PROVIDER_PATHS"
11
- android:resource="@xml/pushy_file_paths" />
12
- </provider>
4
+ <receiver
5
+ android:name="cn.reactnative.modules.update.PackageInstallerStatusReceiver"
6
+ android:exported="false" />
13
7
  </application>
14
8
  </manifest>
@@ -0,0 +1,248 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import android.Manifest;
4
+ import android.app.PendingIntent;
5
+ import android.content.Context;
6
+ import android.content.Intent;
7
+ import android.content.pm.PackageInfo;
8
+ import android.content.pm.PackageInstaller;
9
+ import android.content.pm.PackageManager;
10
+ import android.net.Uri;
11
+ import android.os.Build;
12
+ import android.os.Process;
13
+ import android.provider.Settings;
14
+ import android.util.Log;
15
+ import com.facebook.react.bridge.Promise;
16
+ import com.facebook.react.bridge.ReactApplicationContext;
17
+ import java.io.File;
18
+ import java.io.FileInputStream;
19
+ import java.io.OutputStream;
20
+ import java.util.concurrent.ConcurrentHashMap;
21
+
22
+ final class ApkInstaller {
23
+ private static final int COPY_BUFFER_SIZE = 64 * 1024;
24
+ private static final String SESSION_ID_EXTRA =
25
+ "cn.reactnative.modules.update.extra.PACKAGE_INSTALLER_SESSION_ID";
26
+ private static final ConcurrentHashMap<Integer, Promise> pendingPromises =
27
+ new ConcurrentHashMap<Integer, Promise>();
28
+
29
+ private ApkInstaller() {
30
+ }
31
+
32
+ static boolean ensureInstallPermission(
33
+ ReactApplicationContext reactContext,
34
+ Promise promise
35
+ ) {
36
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
37
+ promise.reject(
38
+ ErrorCodes.UNSUPPORTED_PLATFORM,
39
+ "APK installation requires Android 5.0 or newer"
40
+ );
41
+ return false;
42
+ }
43
+
44
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
45
+ return true;
46
+ }
47
+
48
+ PackageManager packageManager = reactContext.getPackageManager();
49
+ if (!declaresInstallPermission(reactContext, packageManager)) {
50
+ promise.reject(
51
+ ErrorCodes.APK_INSTALL_PERMISSION_REQUIRED,
52
+ "Declare android.permission.REQUEST_INSTALL_PACKAGES in AndroidManifest.xml"
53
+ );
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ if (packageManager.canRequestPackageInstalls()) {
59
+ return true;
60
+ }
61
+
62
+ Intent settingsIntent = new Intent(
63
+ Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
64
+ Uri.parse("package:" + reactContext.getPackageName())
65
+ );
66
+ settingsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
67
+ reactContext.startActivity(settingsIntent);
68
+ promise.reject(
69
+ ErrorCodes.APK_INSTALL_PERMISSION_REQUIRED,
70
+ "Allow this app to install unknown apps in system settings, then retry"
71
+ );
72
+ } catch (Throwable error) {
73
+ promise.reject(
74
+ ErrorCodes.APK_INSTALL_PERMISSION_REQUIRED,
75
+ "Unable to open the unknown app sources settings",
76
+ error
77
+ );
78
+ }
79
+ return false;
80
+ }
81
+
82
+ private static boolean declaresInstallPermission(
83
+ Context context,
84
+ PackageManager packageManager
85
+ ) {
86
+ try {
87
+ PackageInfo packageInfo = packageManager.getPackageInfo(
88
+ context.getPackageName(),
89
+ PackageManager.GET_PERMISSIONS
90
+ );
91
+ String[] requestedPermissions = packageInfo.requestedPermissions;
92
+ if (requestedPermissions == null) {
93
+ return false;
94
+ }
95
+ for (String permission : requestedPermissions) {
96
+ if (Manifest.permission.REQUEST_INSTALL_PACKAGES.equals(permission)) {
97
+ return true;
98
+ }
99
+ }
100
+ } catch (PackageManager.NameNotFoundException error) {
101
+ Log.e(UpdateContext.TAG, "Unable to inspect requested permissions", error);
102
+ }
103
+ return false;
104
+ }
105
+
106
+ static void install(
107
+ ReactApplicationContext reactContext,
108
+ File apkFile,
109
+ String sourceUrl,
110
+ Promise promise
111
+ ) {
112
+ if (!apkFile.isFile() || apkFile.length() <= 0) {
113
+ promise.reject(ErrorCodes.APK_INSTALL_FAILED, "Downloaded APK is empty or missing");
114
+ return;
115
+ }
116
+
117
+ PackageInstaller packageInstaller = reactContext
118
+ .getPackageManager()
119
+ .getPackageInstaller();
120
+ PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(
121
+ PackageInstaller.SessionParams.MODE_FULL_INSTALL
122
+ );
123
+ sessionParams.setAppPackageName(reactContext.getPackageName());
124
+ sessionParams.setSize(apkFile.length());
125
+ if (sourceUrl != null && !sourceUrl.isEmpty()) {
126
+ sessionParams.setOriginatingUri(Uri.parse(sourceUrl));
127
+ }
128
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
129
+ sessionParams.setOriginatingUid(Process.myUid());
130
+ }
131
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
132
+ sessionParams.setInstallReason(PackageManager.INSTALL_REASON_USER);
133
+ }
134
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
135
+ sessionParams.setRequireUserAction(
136
+ PackageInstaller.SessionParams.USER_ACTION_REQUIRED
137
+ );
138
+ }
139
+
140
+ int sessionId = -1;
141
+ boolean committed = false;
142
+ try {
143
+ sessionId = packageInstaller.createSession(sessionParams);
144
+ try (
145
+ PackageInstaller.Session session = packageInstaller.openSession(sessionId);
146
+ FileInputStream input = new FileInputStream(apkFile);
147
+ OutputStream output = session.openWrite("base.apk", 0, apkFile.length())
148
+ ) {
149
+ byte[] buffer = new byte[COPY_BUFFER_SIZE];
150
+ int count;
151
+ while ((count = input.read(buffer)) != -1) {
152
+ output.write(buffer, 0, count);
153
+ }
154
+ session.fsync(output);
155
+
156
+ Intent statusIntent = new Intent(
157
+ reactContext,
158
+ PackageInstallerStatusReceiver.class
159
+ );
160
+ statusIntent.putExtra(SESSION_ID_EXTRA, sessionId);
161
+ int pendingIntentFlags = PendingIntent.FLAG_UPDATE_CURRENT;
162
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
163
+ pendingIntentFlags |= PendingIntent.FLAG_MUTABLE;
164
+ }
165
+ PendingIntent statusPendingIntent = PendingIntent.getBroadcast(
166
+ reactContext,
167
+ sessionId,
168
+ statusIntent,
169
+ pendingIntentFlags
170
+ );
171
+ pendingPromises.put(sessionId, promise);
172
+ session.commit(statusPendingIntent.getIntentSender());
173
+ committed = true;
174
+ }
175
+ } catch (Throwable error) {
176
+ if (sessionId != -1) {
177
+ pendingPromises.remove(sessionId);
178
+ if (!committed) {
179
+ try {
180
+ packageInstaller.abandonSession(sessionId);
181
+ } catch (Throwable abandonError) {
182
+ Log.w(UpdateContext.TAG, "Unable to abandon failed install session", abandonError);
183
+ }
184
+ }
185
+ }
186
+ promise.reject(ErrorCodes.APK_INSTALL_FAILED, "Unable to stage APK installation", error);
187
+ }
188
+ }
189
+
190
+ static void handleStatus(Context context, Intent intent) {
191
+ int sessionId = intent.getIntExtra(SESSION_ID_EXTRA, -1);
192
+ int status = intent.getIntExtra(
193
+ PackageInstaller.EXTRA_STATUS,
194
+ PackageInstaller.STATUS_FAILURE
195
+ );
196
+
197
+ if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
198
+ Intent confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT);
199
+ if (confirmationIntent == null) {
200
+ rejectPending(sessionId, "System installer did not provide a confirmation screen");
201
+ return;
202
+ }
203
+ try {
204
+ confirmationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
205
+ context.startActivity(confirmationIntent);
206
+ resolvePending(sessionId);
207
+ } catch (Throwable error) {
208
+ rejectPending(sessionId, "Unable to open the system install confirmation", error);
209
+ }
210
+ return;
211
+ }
212
+
213
+ if (status == PackageInstaller.STATUS_SUCCESS) {
214
+ resolvePending(sessionId);
215
+ return;
216
+ }
217
+
218
+ String statusMessage = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE);
219
+ rejectPending(
220
+ sessionId,
221
+ statusMessage == null ? "Package installation failed" : statusMessage
222
+ );
223
+ }
224
+
225
+ private static void resolvePending(int sessionId) {
226
+ Promise promise = pendingPromises.remove(sessionId);
227
+ if (promise != null) {
228
+ promise.resolve(null);
229
+ }
230
+ }
231
+
232
+ private static void rejectPending(int sessionId, String message) {
233
+ rejectPending(sessionId, message, null);
234
+ }
235
+
236
+ private static void rejectPending(int sessionId, String message, Throwable error) {
237
+ Promise promise = pendingPromises.remove(sessionId);
238
+ if (promise != null) {
239
+ if (error == null) {
240
+ promise.reject(ErrorCodes.APK_INSTALL_FAILED, message);
241
+ } else {
242
+ promise.reject(ErrorCodes.APK_INSTALL_FAILED, message, error);
243
+ }
244
+ } else {
245
+ Log.e(UpdateContext.TAG, message, error);
246
+ }
247
+ }
248
+ }
@@ -752,7 +752,7 @@ class DownloadTask implements Runnable {
752
752
 
753
753
  // The task itself succeeded. Run the completion callback outside the
754
754
  // try/catch above so an exception thrown by the callback (e.g. a
755
- // FileProvider misconfiguration during installApk) is not mistaken for
755
+ // PackageInstaller failure during APK staging) is not mistaken for
756
756
  // a download failure that deletes the successfully downloaded file and
757
757
  // settles the promise a second time.
758
758
  if (params.listener != null) {
@@ -19,6 +19,8 @@ final class ErrorCodes {
19
19
  static final String RESET_FAILED = "RESET_FAILED";
20
20
  static final String INVALID_HASH_INFO = "INVALID_HASH_INFO";
21
21
  static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM";
22
+ static final String APK_INSTALL_PERMISSION_REQUIRED = "APK_INSTALL_PERMISSION_REQUIRED";
23
+ static final String APK_INSTALL_FAILED = "APK_INSTALL_FAILED";
22
24
 
23
25
  private ErrorCodes() {
24
26
  }
@@ -0,0 +1,12 @@
1
+ package cn.reactnative.modules.update;
2
+
3
+ import android.content.BroadcastReceiver;
4
+ import android.content.Context;
5
+ import android.content.Intent;
6
+
7
+ public final class PackageInstallerStatusReceiver extends BroadcastReceiver {
8
+ @Override
9
+ public void onReceive(Context context, Intent intent) {
10
+ ApkInstaller.handleStatus(context, intent);
11
+ }
12
+ }
@@ -5,7 +5,6 @@ import android.content.SharedPreferences;
5
5
  import android.content.pm.PackageInfo;
6
6
  import android.content.pm.PackageManager;
7
7
  import android.os.Build;
8
- import android.os.Environment;
9
8
  import android.util.Log;
10
9
  import com.facebook.react.ReactInstanceManager;
11
10
  import java.io.File;
@@ -281,13 +280,7 @@ public class UpdateContext {
281
280
  params.hash = hash;
282
281
  params.listener = listener;
283
282
 
284
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && fileName.equals("update.apk")) {
285
- params.targetFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "pushy_update.apk");
286
-
287
- } else {
288
- params.targetFile = new File(rootDir, fileName);
289
-
290
- }
283
+ params.targetFile = new File(rootDir, fileName);
291
284
  // params.unzipDirectory = new File(rootDir, hash);
292
285
  enqueue(params);
293
286
  }
@@ -61,14 +61,16 @@ public class UpdateModuleImpl {
61
61
  final ReadableMap options,
62
62
  final Promise promise
63
63
  ) {
64
- String url = options.getString("url");
64
+ final String url = options.getString("url");
65
65
  String hash = options.getString("hash");
66
66
  String target = options.getString("target");
67
+ if (!ApkInstaller.ensureInstallPermission(reactContext, promise)) {
68
+ return;
69
+ }
67
70
  updateContext.downloadFile(url, hash, target, new UpdateContext.DownloadFileListener() {
68
71
  @Override
69
72
  public void onDownloadCompleted(DownloadTaskParams params) {
70
- UpdateModuleSupport.installApk(reactContext, params.targetFile);
71
- promise.resolve(null);
73
+ ApkInstaller.install(reactContext, params.targetFile, url, promise);
72
74
  }
73
75
 
74
76
  @Override
@@ -1,13 +1,6 @@
1
1
  package cn.reactnative.modules.update;
2
2
 
3
- import static androidx.core.content.FileProvider.getUriForFile;
4
-
5
- import android.content.Intent;
6
- import android.net.Uri;
7
- import android.os.Build;
8
3
  import android.util.Log;
9
- import com.facebook.react.bridge.ReactApplicationContext;
10
- import java.io.File;
11
4
  import java.util.HashMap;
12
5
  import java.util.Map;
13
6
 
@@ -48,27 +41,4 @@ final class UpdateModuleSupport {
48
41
  constants.put("supportedDiffVersion", supportedDiffVersion);
49
42
  return constants;
50
43
  }
51
-
52
- static void installApk(ReactApplicationContext reactContext, File toInstall) {
53
- Uri apkUri;
54
- Intent intent;
55
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
56
- apkUri = getUriForFile(
57
- reactContext,
58
- reactContext.getPackageName() + ".pushy.fileprovider",
59
- toInstall
60
- );
61
- intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
62
- intent.setData(apkUri);
63
- intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
64
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
65
- } else {
66
- apkUri = Uri.fromFile(toInstall);
67
- intent = new Intent(Intent.ACTION_VIEW);
68
- intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
69
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
70
- }
71
-
72
- reactContext.startActivity(intent);
73
- }
74
44
  }
@@ -39,6 +39,12 @@ constexpr const char* kInvalidHashInfo = "INVALID_HASH_INFO";
39
39
  // The method is not supported on this platform (e.g. downloadAndInstallApk
40
40
  // outside Android).
41
41
  constexpr const char* kUnsupportedPlatform = "UNSUPPORTED_PLATFORM";
42
+ // Android full-package installation requires the user to trust the app as an
43
+ // install source before PackageInstaller can accept a session.
44
+ constexpr const char* kApkInstallPermissionRequired =
45
+ "APK_INSTALL_PERMISSION_REQUIRED";
46
+ // PackageInstaller could not create, write, commit, or continue an APK session.
47
+ constexpr const char* kApkInstallFailed = "APK_INSTALL_FAILED";
42
48
 
43
49
  } // namespace error_codes
44
50
  } // namespace pushy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-update",
3
- "version": "10.52.3",
3
+ "version": "10.53.0",
4
4
  "description": "react-native hot update",
5
5
  "main": "src/index",
6
6
  "types": "src/index.ts",
package/src/client.ts CHANGED
@@ -26,7 +26,6 @@ import {
26
26
  type UpdateErrorCode,
27
27
  } from './error';
28
28
  import i18n from './i18n';
29
- import { PermissionsAndroid } from './permissions';
30
29
  import {
31
30
  resolveServerEventHash,
32
31
  resolveServerEventType,
@@ -1302,27 +1301,6 @@ export class Pushy {
1302
1301
  this.throwIfEnabled(err);
1303
1302
  return;
1304
1303
  }
1305
- if (Platform.Version <= 23) {
1306
- try {
1307
- const granted = await PermissionsAndroid.request(
1308
- PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
1309
- );
1310
- if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
1311
- const err = new UpdateError(
1312
- this.t('error_storage_permission_rejected'),
1313
- 'STORAGE_PERMISSION_REJECTED'
1314
- );
1315
- this.emitError(err, 'rejectStoragePermission');
1316
- this.throwIfEnabled(err);
1317
- return;
1318
- }
1319
- } catch (e: any) {
1320
- const err = toUpdateError(e, 'STORAGE_PERMISSION_ERROR');
1321
- this.emitError(err, 'errorStoragePermission');
1322
- this.throwIfEnabled(err);
1323
- return;
1324
- }
1325
- }
1326
1304
  sharedState.apkStatus = 'downloading';
1327
1305
  this.report({ type: 'downloadingApk' });
1328
1306
  const progressKey = 'downloadingApk';
package/src/error.ts CHANGED
@@ -34,7 +34,9 @@ export type UpdateErrorCode =
34
34
  | 'RESTART_FAILED'
35
35
  | 'RESET_FAILED'
36
36
  | 'INVALID_HASH_INFO'
37
- | 'UNSUPPORTED_PLATFORM';
37
+ | 'UNSUPPORTED_PLATFORM'
38
+ | 'APK_INSTALL_PERMISSION_REQUIRED'
39
+ | 'APK_INSTALL_FAILED';
38
40
 
39
41
  const KNOWN_CODES = new Set<string>([
40
42
  'MODULE_NOT_LOADED',
@@ -58,6 +60,8 @@ const KNOWN_CODES = new Set<string>([
58
60
  'RESET_FAILED',
59
61
  'INVALID_HASH_INFO',
60
62
  'UNSUPPORTED_PLATFORM',
63
+ 'APK_INSTALL_PERMISSION_REQUIRED',
64
+ 'APK_INSTALL_FAILED',
61
65
  ]);
62
66
 
63
67
  /**
@@ -1,14 +0,0 @@
1
- package cn.reactnative.modules.update;
2
-
3
- import androidx.core.content.FileProvider;
4
-
5
- /**
6
- * Providing a custom {@code FileProvider} prevents manifest {@code <provider>} name collisions.
7
- *
8
- * See https://developer.android.com/guide/topics/manifest/provider-element.html for details.
9
- */
10
- public class PushyFileProvider extends FileProvider {
11
-
12
- // This class intentionally left blank.
13
-
14
- }
@@ -1,5 +0,0 @@
1
- <paths xmlns:android="http://schemas.android.com/apk/res/android">
2
- <files-path
3
- name="pushy"
4
- path="." />
5
- </paths>
@@ -1 +0,0 @@
1
- export { PermissionsAndroid } from 'react-native';
@@ -1,8 +0,0 @@
1
- // RN >= 0.87 (generated types) no longer exports the `PermissionsAndroidStatic`
2
- // interface, so derive the shape from the exported value instead — that spelling
3
- // works on every RN version we support.
4
- import type { PermissionsAndroid as RNPermissionsAndroid } from 'react-native';
5
- import { emptyModule } from './utils';
6
-
7
- export const PermissionsAndroid =
8
- emptyModule as unknown as typeof RNPermissionsAndroid;