mirror of
https://github.com/zhigang1992/react-native-code-push.git
synced 2026-05-19 19:39:54 +08:00
Added support react-native-windows dotNet Added example for react-native-windows UWP based Added example for react-native-windows dotNet based Project structure: CodePush.Shared - shared code between UWP and dotNet CodePush - UWP specific code CodePush.Net46 - dotNet specific code For UWP solution it needs to be added the following projects: CodePush.Shared CodePush For dotNet solution it needs to be added the following projects: CodePush.Shared CodePush.Net46 Examples: Examples\CodePushDemoApp\windows\CodePushDemoApp.sln the solution contains both examples (UWP and dotNet). Notes Example for ARM configuration has not been tested. Since there is no changes in UWP part of implementation, there is low risk of failure. In this implementation we tried to reuse UWP library as much as possible. The following issues are relevant for both platforms: ZipFile.ExtractToDirectory is not reliable and throws exception if: folder exists already path is too long (> 250 chars) Un-zipping is quite long operation. Does it make sense for async? await UpdateUtils.UnzipBundleAsync(downloadFile.Path, unzippedFolder.Path);
275 lines
11 KiB
C#
275 lines
11 KiB
C#
using Newtonsoft.Json.Linq;
|
|
using ReactNative;
|
|
using ReactNative.Bridge;
|
|
using ReactNative.Modules.Core;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
#if WINDOWS_UWP
|
|
using Windows.Web.Http;
|
|
#else
|
|
using CodePush.Net46.Adapters.Http;
|
|
#endif
|
|
|
|
namespace CodePush.ReactNative
|
|
{
|
|
internal class CodePushNativeModule : ReactContextNativeModuleBase
|
|
{
|
|
private CodePushReactPackage _codePush;
|
|
private MinimumBackgroundListener _minimumBackgroundListener;
|
|
private ReactContext _reactContext;
|
|
|
|
public CodePushNativeModule(ReactContext reactContext, CodePushReactPackage codePush) : base(reactContext)
|
|
{
|
|
_reactContext = reactContext;
|
|
_codePush = codePush;
|
|
}
|
|
|
|
public override string Name
|
|
{
|
|
get
|
|
{
|
|
return "CodePush";
|
|
}
|
|
}
|
|
|
|
public override IReadOnlyDictionary<string, object> Constants
|
|
{
|
|
get
|
|
{
|
|
return new Dictionary<string, object>
|
|
{
|
|
{ "codePushInstallModeImmediate", InstallMode.Immediate },
|
|
{ "codePushInstallModeOnNextResume", InstallMode.OnNextResume },
|
|
{ "codePushInstallModeOnNextRestart", InstallMode.OnNextRestart },
|
|
{ "codePushUpdateStateRunning", UpdateState.Running },
|
|
{ "codePushUpdateStatePending", UpdateState.Pending },
|
|
{ "codePushUpdateStateLatest", UpdateState.Latest },
|
|
};
|
|
}
|
|
}
|
|
|
|
public override void Initialize()
|
|
{
|
|
_codePush.InitializeUpdateAfterRestart();
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void downloadUpdate(JObject updatePackage, bool notifyProgress, IPromise promise)
|
|
{
|
|
try
|
|
{
|
|
updatePackage[CodePushConstants.BinaryModifiedTimeKey] = "" + await FileUtils.GetBinaryResourcesModifiedTimeAsync(_codePush.AssetsBundleFileName).ConfigureAwait(false);
|
|
await _codePush.UpdateManager.DownloadPackageAsync(
|
|
updatePackage,
|
|
_codePush.AssetsBundleFileName,
|
|
new Progress<HttpProgress>(
|
|
(HttpProgress progress) =>
|
|
{
|
|
if (!notifyProgress)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var downloadProgress = new JObject()
|
|
{
|
|
{ "totalBytes", progress.TotalBytesToReceive },
|
|
{ "receivedBytes", progress.BytesReceived }
|
|
};
|
|
|
|
_reactContext
|
|
.GetJavaScriptModule<RCTDeviceEventEmitter>()
|
|
.emit(CodePushConstants.DownloadProgressEventName, downloadProgress);
|
|
}
|
|
)
|
|
).ConfigureAwait(false);
|
|
|
|
JObject newPackage = await _codePush.UpdateManager.GetPackageAsync((string)updatePackage[CodePushConstants.PackageHashKey]).ConfigureAwait(false);
|
|
promise.Resolve(newPackage);
|
|
}
|
|
catch (InvalidDataException e)
|
|
{
|
|
CodePushUtils.Log(e.ToString());
|
|
SettingsManager.SaveFailedUpdate(updatePackage);
|
|
promise.Reject(e);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
CodePushUtils.Log(e.ToString());
|
|
promise.Reject(e);
|
|
}
|
|
}
|
|
|
|
[ReactMethod]
|
|
public void getConfiguration(IPromise promise)
|
|
{
|
|
var config = new JObject
|
|
{
|
|
{ "appVersion", _codePush.AppVersion },
|
|
{ "clientUniqueId", CodePushUtils.GetDeviceId() },
|
|
{ "deploymentKey", _codePush.DeploymentKey },
|
|
{ "serverUrl", CodePushConstants.CodePushServerUrl }
|
|
};
|
|
|
|
// TODO generate binary hash
|
|
// string binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode);
|
|
/*if (binaryHash != null)
|
|
{
|
|
configMap.putString(PACKAGE_HASH_KEY, binaryHash);
|
|
}*/
|
|
promise.Resolve(config);
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void getUpdateMetadata(UpdateState updateState, IPromise promise)
|
|
{
|
|
JObject currentPackage = await _codePush.UpdateManager.GetCurrentPackageAsync().ConfigureAwait(false);
|
|
if (currentPackage == null)
|
|
{
|
|
promise.Resolve("");
|
|
return;
|
|
}
|
|
|
|
var currentUpdateIsPending = false;
|
|
|
|
if (currentPackage[CodePushConstants.PackageHashKey] != null)
|
|
{
|
|
var currentHash = (string)currentPackage[CodePushConstants.PackageHashKey];
|
|
currentUpdateIsPending = SettingsManager.IsPendingUpdate(currentHash);
|
|
}
|
|
|
|
if (updateState == UpdateState.Pending && !currentUpdateIsPending)
|
|
{
|
|
// The caller wanted a pending update
|
|
// but there isn't currently one.
|
|
promise.Resolve("");
|
|
}
|
|
else if (updateState == UpdateState.Running && currentUpdateIsPending)
|
|
{
|
|
// The caller wants the running update, but the current
|
|
// one is pending, so we need to grab the previous.
|
|
promise.Resolve(await _codePush.UpdateManager.GetPreviousPackageAsync().ConfigureAwait(false));
|
|
}
|
|
else
|
|
{
|
|
// The current package satisfies the request:
|
|
// 1) Caller wanted a pending, and there is a pending update
|
|
// 2) Caller wanted the running update, and there isn't a pending
|
|
// 3) Caller wants the latest update, regardless if it's pending or not
|
|
if (_codePush.IsRunningBinaryVersion)
|
|
{
|
|
// This only matters in Debug builds. Since we do not clear "outdated" updates,
|
|
// we need to indicate to the JS side that somehow we have a current update on
|
|
// disk that is not actually running.
|
|
currentPackage["_isDebugOnly"] = true;
|
|
}
|
|
|
|
// Enable differentiating pending vs. non-pending updates
|
|
currentPackage["isPending"] = currentUpdateIsPending;
|
|
promise.Resolve(currentPackage);
|
|
}
|
|
}
|
|
|
|
|
|
[ReactMethod]
|
|
public void getNewStatusReport(IPromise promise)
|
|
{
|
|
// TODO implement this
|
|
promise.Resolve("");
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void installUpdate(JObject updatePackage, InstallMode installMode, int minimumBackgroundDuration, IPromise promise)
|
|
{
|
|
await _codePush.UpdateManager.InstallPackageAsync(updatePackage, SettingsManager.IsPendingUpdate(null)).ConfigureAwait(false);
|
|
var pendingHash = (string)updatePackage[CodePushConstants.PackageHashKey];
|
|
SettingsManager.SavePendingUpdate(pendingHash, /* isLoading */false);
|
|
if (installMode == InstallMode.OnNextResume)
|
|
{
|
|
if (_minimumBackgroundListener == null)
|
|
{
|
|
// Ensure we do not add the listener twice.
|
|
Action loadBundleAction = () =>
|
|
{
|
|
Context.RunOnNativeModulesQueueThread(async () =>
|
|
{
|
|
await LoadBundleAsync().ConfigureAwait(false);
|
|
});
|
|
};
|
|
|
|
_minimumBackgroundListener = new MinimumBackgroundListener(loadBundleAction, minimumBackgroundDuration);
|
|
_reactContext.AddLifecycleEventListener(_minimumBackgroundListener);
|
|
}
|
|
else
|
|
{
|
|
_minimumBackgroundListener.MinimumBackgroundDuration = minimumBackgroundDuration;
|
|
}
|
|
}
|
|
|
|
promise.Resolve("");
|
|
}
|
|
|
|
[ReactMethod]
|
|
public void isFailedUpdate(string packageHash, IPromise promise)
|
|
{
|
|
promise.Resolve(SettingsManager.IsFailedHash(packageHash));
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void isFirstRun(string packageHash, IPromise promise)
|
|
{
|
|
bool isFirstRun = _codePush.DidUpdate
|
|
&& packageHash != null
|
|
&& packageHash.Length > 0
|
|
&& packageHash.Equals(await _codePush.UpdateManager.GetCurrentPackageHashAsync().ConfigureAwait(false));
|
|
promise.Resolve(isFirstRun);
|
|
}
|
|
|
|
[ReactMethod]
|
|
public void notifyApplicationReady(IPromise promise)
|
|
{
|
|
SettingsManager.RemovePendingUpdate();
|
|
promise.Resolve("");
|
|
}
|
|
|
|
[ReactMethod]
|
|
public async void restartApp(bool onlyIfUpdateIsPending)
|
|
{
|
|
// If this is an unconditional restart request, or there
|
|
// is current pending update, then reload the app.
|
|
if (!onlyIfUpdateIsPending || SettingsManager.IsPendingUpdate(null))
|
|
{
|
|
await LoadBundleAsync().ConfigureAwait(false);
|
|
}
|
|
}
|
|
|
|
internal async Task LoadBundleAsync()
|
|
{
|
|
// #1) Get the private ReactInstanceManager, which is what includes
|
|
// the logic to reload the current React context.
|
|
FieldInfo info = typeof(ReactPage)
|
|
.GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance);
|
|
#if WINDOWS_UWP
|
|
var reactInstanceManager = (ReactInstanceManager)typeof(ReactPage)
|
|
.GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.GetValue(_codePush.MainPage);
|
|
#else
|
|
var reactInstanceManager = ((Lazy<IReactInstanceManager>)typeof(ReactPage)
|
|
.GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.GetValue(_codePush.MainPage)).Value as ReactInstanceManager;
|
|
#endif
|
|
|
|
// #2) Update the locally stored JS bundle file path
|
|
Type reactInstanceManagerType = typeof(ReactInstanceManager);
|
|
string latestJSBundleFile = await _codePush.GetJavaScriptBundleFileAsync(_codePush.AssetsBundleFileName).ConfigureAwait(false);
|
|
reactInstanceManagerType
|
|
.GetField("_jsBundleFile", BindingFlags.NonPublic | BindingFlags.Instance)
|
|
.SetValue(reactInstanceManager, latestJSBundleFile);
|
|
|
|
// #3) Get the context creation method and fire it on the UI thread (which RN enforces)
|
|
Context.RunOnDispatcherQueueThread(reactInstanceManager.RecreateReactContextInBackground);
|
|
}
|
|
}
|
|
} |