using Newtonsoft.Json.Linq; using ReactNative; using ReactNative.Bridge; using ReactNative.Modules.Core; using System; using System.Collections.Generic; using System.Reflection; using System.Threading.Tasks; using Windows.Web.Http; namespace CodePush.ReactNative { internal class CodePushNativeModule : ReactContextNativeModuleBase { private CodePushResumeListener _codePushLifecycleEventListener = null; private ReactContext _reactContext; private CodePushReactPackage _codePush; public CodePushNativeModule(ReactContext reactContext, CodePushReactPackage codePush) : base(reactContext) { _reactContext = reactContext; _codePush = codePush; } public override string Name { get { return "CodePush"; } } public override IReadOnlyDictionary Constants { get { return new Dictionary { { "codePushInstallModeImmediate", InstallMode.ON_NEXT_RESTART }, { "codePushInstallModeOnNextResume", InstallMode.ON_NEXT_RESUME }, { "codePushInstallModeOnNextRestart", InstallMode.ON_NEXT_RESTART }, { "codePushUpdateStateRunning", UpdateState.RUNNING }, { "codePushUpdateStatePending", UpdateState.PENDING }, { "codePushUpdateStateLatest", UpdateState.LATEST }, }; } } public override void Initialize() { _codePush.InitializeUpdateAfterRestart(); } [ReactMethod] public void downloadUpdate(JObject updatePackage, bool notifyProgress, IPromise promise) { Action downloadAction = async () => { try { updatePackage[CodePushConstants.BinaryModifiedTimeKey] = "" + await _codePush.GetBinaryResourcesModifiedTime(); await _codePush.UpdateManager.DownloadPackage( updatePackage, _codePush.AssetsBundleFileName, new Progress( (HttpProgress progress) => { if (!notifyProgress) { return; } var downloadProgress = new JObject(); downloadProgress["totalBytes"] = progress.TotalBytesToReceive; downloadProgress["receivedBytes"] = progress.BytesReceived; _reactContext .GetJavaScriptModule() .emit(CodePushConstants.DownloadProgressEventName, downloadProgress); } ) ); JObject newPackage = await _codePush.UpdateManager.GetPackage((string)updatePackage[CodePushConstants.PackageHashKey]); promise.Resolve(newPackage); } catch (CodePushInvalidUpdateException e) { CodePushUtils.Log(e.ToString()); SettingsManager.SaveFailedUpdate(updatePackage); promise.Reject(e); } catch (Exception e) { CodePushUtils.Log(e.ToString()); promise.Reject(e); } }; Context.RunOnNativeModulesQueueThread(downloadAction); } [ReactMethod] public void getConfiguration(IPromise promise) { var config = new JObject { { "appVersion", _codePush.AppVersion }, { "deploymentKey", _codePush.DeploymentKey }, { "serverUrl", CodePushConstants.CodePushServerUrl }, { "clientUniqueId", CodePushUtils.GetDeviceId() }, }; // TODO generate binary hash // string binaryHash = CodePushUpdateUtils.getHashForBinaryContents(mainActivity, isDebugMode); /*if (binaryHash != null) { configMap.putString(PACKAGE_HASH_KEY, binaryHash); }*/ promise.Resolve(config); } [ReactMethod] public void getUpdateMetadata(int updateState, IPromise promise) { Action getCurrentPackageAction = async () => { JObject currentPackage = await _codePush.UpdateManager.GetCurrentPackage(); if (currentPackage == null) { promise.Resolve(""); return; } var currentUpdateIsPending = false; if (currentPackage[CodePushConstants.PackageHashKey] != null) { var currentHash = (string)currentPackage[CodePushConstants.PackageHashKey]; currentUpdateIsPending = _codePush.IsPendingUpdate(currentHash); } if (updateState == (int)UpdateState.PENDING && !currentUpdateIsPending) { // The caller wanted a pending update // but there isn't currently one. promise.Resolve(""); } else if (updateState == (int)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.GetPreviousPackage()); } 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); } }; Context.RunOnNativeModulesQueueThread(getCurrentPackageAction); } [ReactMethod] public void getNewStatusReport(IPromise promise) { // TODO implement this promise.Resolve(""); } [ReactMethod] public void installUpdate(JObject updatePackage, int installMode, int minimumBackgroundDuration, IPromise promise) { Action installUpdateAction = async () => { await _codePush.UpdateManager.InstallPackage(updatePackage, _codePush.IsPendingUpdate(null)); var pendingHash = (string)updatePackage[CodePushConstants.PackageHashKey]; SettingsManager.SavePendingUpdate(pendingHash, /* isLoading */false); if (installMode == (int)InstallMode.ON_NEXT_RESUME) { if (_codePushLifecycleEventListener == null) { // Ensure we do not add the listener twice. _codePushLifecycleEventListener = new CodePushResumeListener(this, minimumBackgroundDuration); _reactContext.AddLifecycleEventListener(_codePushLifecycleEventListener); } else { _codePushLifecycleEventListener.MinimumBackgroundDuration = minimumBackgroundDuration; } } promise.Resolve(""); }; Context.RunOnNativeModulesQueueThread(installUpdateAction); } [ReactMethod] public void isFailedUpdate(string packageHash, IPromise promise) { promise.Resolve(_codePush.IsFailedHash(packageHash)); } [ReactMethod] public void isFirstRun(string packageHash, IPromise promise) { Action isFirstRunAction = async () => { bool isFirstRun = _codePush.DidUpdate && packageHash != null && packageHash.Length > 0 && packageHash.Equals(await _codePush.UpdateManager.GetCurrentPackageHash()); promise.Resolve(isFirstRun); }; Context.RunOnNativeModulesQueueThread(isFirstRunAction); } [ReactMethod] public void notifyApplicationReady(IPromise promise) { SettingsManager.RemovePendingUpdate(); promise.Resolve(""); } [ReactMethod] public void restartApp(bool onlyIfUpdateIsPending) { Action restartAppAction = async () => { // If this is an unconditional restart request, or there // is current pending update, then reload the app. if (!onlyIfUpdateIsPending || _codePush.IsPendingUpdate(null)) { await LoadBundle(); } }; Context.RunOnNativeModulesQueueThread(restartAppAction); } internal async Task LoadBundle() { // #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); var reactInstanceManager = (ReactInstanceManager)typeof(ReactPage) .GetField("_reactInstanceManager", BindingFlags.NonPublic | BindingFlags.Instance) .GetValue(_codePush.MainPage); // #2) Update the locally stored JS bundle file path Type reactInstanceManagerType = typeof(ReactInstanceManager); string latestJSBundleFile = await _codePush.GetJavaScriptBundleFileAsync(_codePush.AssetsBundleFileName); 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); } } }