Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lateinit property requestPermission has not been initialized #71

Closed
mohamadfala opened this issue Mar 6, 2024 · 13 comments · Fixed by #110
Closed

lateinit property requestPermission has not been initialized #71

mohamadfala opened this issue Mar 6, 2024 · 13 comments · Fixed by #110

Comments

@mohamadfala
Copy link

Describe the bug
I've followed the exact installation in your documentation on how to use it with expo.
I'm using an expo dev build with config plugin.

Tried to run the app on Android 13 and 10, both encountered the same error.
Downgraded to version 1.2.3 and it is now working.

However, I'd like to report the issue so I can upgrade to the latest version.

Here's the error log:

Your app just crashed. See the error below. kotlin.UninitializedPropertyAccessException: lateinit property requestPermission has not been initialized dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate.launch(HealthConnectPermissionDelegate.kt:32) dev.matinzd.healthconnect.HealthConnectManager$requestPermission$1$1.invokeSuspend(HealthConnectManager.kt:64) kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108) kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115) kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103) kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697) kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)

Screenshot 2024-03-06 at 7 09 17 PM

Environment:

  • Health Connect Version: 2.0.1
  • React Native Version: 0.73.5
@abhaynpai
Copy link

+1

@xseignard
Copy link

I confirm the bug too

@matinzd
Copy link
Owner

matinzd commented Mar 9, 2024

This is not a bug. You have to do additional configuration in your MainActivity file which is not available with Expo.
Refer to the release details for more info:

https://github.com/matinzd/react-native-health-connect/releases/tag/v2.0.0

Try to rebuild your Android folder with dev client and add these configurations manually to MainActivity.
Or you can write your own ezpo config plugin.

@xseignard
Copy link

xseignard commented Mar 9, 2024

Hey @matinzd , indeed

I created my own custom expo config plugin for that inspired by #70:

import { ConfigPlugin, withAndroidManifest, withDangerousMod, withMainActivity } from "@expo/config-plugins";
import path from "node:path";
import { promises as fs } from "node:fs";

type InsertProps = {
  /** The content to look at */
  content: string;
  /** The string to find within `content` */
  toFind: string;
  /** What to insert in the `content`, be it a single string, or an array of string that would be separated by a `\n` */
  toInsert: string | string[];
  /** A tag that will be used to keep track of which `expo-plugin-config` introduced the modification */
  tag: string;
  /** The symbol(s) to be used to begin a comment in the given `content`. If an array, the first item will be used to start the comment, the second to end it */
  commentSymbol: string | [string, string];
};

const createCommentSymbols = (commentSymbol: InsertProps["commentSymbol"]) => {
  return {
    startCommentSymbol: Array.isArray(commentSymbol) ? commentSymbol[0] : commentSymbol,
    endCommentSymbol: Array.isArray(commentSymbol) ? ` ${commentSymbol[1]}` : "",
  };
};

const createStartTag = (
  commentSymbol: InsertProps["commentSymbol"],
  tag: InsertProps["tag"],
  toInsert: InsertProps["toInsert"],
) => {
  const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
  return `${startCommentSymbol} @generated begin ${tag} - expo prebuild (DO NOT MODIFY)${endCommentSymbol}`;
};

const createEndTag = (commentSymbol: InsertProps["commentSymbol"], tag: InsertProps["tag"]) => {
  const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
  return `${startCommentSymbol} @generated end ${tag}${endCommentSymbol}`;
};

const createContentToInsert = (
  commentSymbol: InsertProps["commentSymbol"],
  tag: InsertProps["tag"],
  toInsert: InsertProps["toInsert"],
) => {
  const startTag = createStartTag(commentSymbol, tag, toInsert);
  const endTag = createEndTag(commentSymbol, tag);
  return `${startTag}\n${Array.isArray(toInsert) ? toInsert.join("\n") : toInsert}\n${endTag}`;
};

const insert = ({
  content,
  toFind,
  toInsert,
  tag,
  commentSymbol,
  where,
}: InsertProps & {
  where: "before" | "after" | "replace";
}): string => {
  const toInsertWithComments = createContentToInsert(commentSymbol, tag, toInsert);
  if (!content.includes(toFind)) {
    throw new Error(`Couldn't find ${toFind} in the given props.content`);
  }
  if (!content.includes(toInsertWithComments)) {
    switch (where) {
      case "before":
        content = content.replace(toFind, `${toInsertWithComments}\n${toFind}`);
        break;
      case "after":
        content = content.replace(toFind, `${toFind}\n${toInsertWithComments}`);
        break;
      case "replace":
        content = content.replace(toFind, `${toInsertWithComments}`);
        break;
    }
  }
  return content;
};

/**
 * Insert `props.toInsert` into `props.content` the line after `props.toFind`
 * @returns the modified `props.content`
 */
export const insertAfter = (props: InsertProps) => {
  return insert({ ...props, where: "after" });
};

/**
 * Insert `props.toInsert` into `props.content` the line before `props.toFind`
 * @returns the modified `props.content`
 */
export const insertBefore = (props: InsertProps) => {
  return insert({ ...props, where: "before" });
};

/**
 * Replace `props.toFind` by `props.toInsert` into `props.content`
 * @returns the modified `props.content`
 */
export const replace = (props: InsertProps) => {
  return insert({ ...props, where: "replace" });
};

/** Copies `srcFile` to `destFolder` with an optional `destFileName` or its initial name if not provided
 * @returns the path of the created file
 */
const copyFile = async (srcFile: string, destFolder: string, destFileName?: string) => {
  const fileName = destFileName ?? path.basename(srcFile);
  await fs.mkdir(destFolder, { recursive: true });
  const destFile = path.resolve(destFolder, fileName);
  await fs.copyFile(srcFile, destFile);
  return destFile;
};

const withReactNativeHealthConnect: ConfigPlugin<{
  permissionsRationaleActivityPath: string;
}> = (config, { permissionsRationaleActivityPath }) => {
  config = withAndroidManifest(config, async (config) => {
    const androidManifest = config.modResults.manifest;
    if (!androidManifest?.application?.[0]) {
      throw new Error("AndroidManifest.xml is not valid!");
    }
    if (!androidManifest.application[0]["activity"]) {
      throw new Error("AndroidManifest.xml is missing application activity");
    }
    androidManifest.application[0]["activity"].push({
      $: {
        "android:name": ".PermissionsRationaleActivity",
        "android:exported": "true",
      },
      "intent-filter": [
        {
          action: [{ $: { "android:name": "androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" } }],
        },
      ],
    });
    // @ts-expect-error activity-alias is not defined in the type
    if (!androidManifest.application[0]["activity-alias"]) {
      // @ts-expect-error activity-alias is not defined in the type
      androidManifest.application[0]["activity-alias"] = [];
    }
    // @ts-expect-error activity-alias is not defined in the type
    androidManifest.application[0]["activity-alias"].push({
      $: {
        "android:name": "ViewPermissionUsageActivity",
        "android:exported": "true",
        "android:targetActivity": ".PermissionsRationaleActivity",
        "android:permission": "android.permission.START_VIEW_PERMISSION_USAGE",
      },
      "intent-filter": [
        {
          action: [{ $: { "android:name": "android.intent.action.VIEW_PERMISSION_USAGE" } }],
          category: [{ $: { "android:name": "android.intent.category.HEALTH_PERMISSIONS" } }],
        },
      ],
    });

    return config;
  });
 
  config = withMainActivity(config, async (config) => {
    config.modResults.contents = insertAfter({
      content: config.modResults.contents,
      toFind: "import com.facebook.react.defaults.DefaultReactActivityDelegate;",
      toInsert: "import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate;",
      commentSymbol: "//",
      tag: "withReactNativeHealthConnect",
    });
    config.modResults.contents = insertAfter({
      content: config.modResults.contents,
      toFind: "super.onCreate(null);",
      toInsert: [
        "HealthConnectPermissionDelegate hcpd = HealthConnectPermissionDelegate.INSTANCE;",
        'hcpd.setPermissionDelegate(this, "com.google.android.apps.healthdata");',
      ],
      commentSymbol: "//",
      tag: "withReactNativeHealthConnect",
    });
    return config;
  });

  config = withDangerousMod(config, [
    "android",
    async (config) => {
      const projectRoot = config.modRequest.projectRoot;
      const destPath = path.resolve(projectRoot, "android/app/src/main/java/com/alanmobile");
      await copyFile(permissionsRationaleActivityPath, destPath, "PermissionsRationaleActivity.kt");
      return config;
    },
  ]);

  return config;
};

export default withReactNativeHealthConnect;

Should be straightforward to update the expo config plugin in this repo from this one

@alexandre-kakal
Copy link

Does this plugin still fix the issue ?
I copied it at the root of my project, then ran npx tsc androidManifestPlugin.ts --skipLibCheck.
And add the compiled file inside my app.json plugin array.

However, when it comes to build the app, I caught this error :

Cannot read properties of undefined (reading 'permissionsRationaleActivityPath')

@yukukotani
Copy link

My MainActivity is written in Kotlin but not Java, so I've customized the plugin.

// https://github.com/matinzd/react-native-health-connect/issues/71#issuecomment-1986791229

import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { ConfigPlugin, withAndroidManifest, withDangerousMod, withMainActivity } from '@expo/config-plugins';

type InsertProps = {
  /** The content to look at */
  content: string;
  /** The string to find within `content` */
  toFind: string;
  /** What to insert in the `content`, be it a single string, or an array of string that would be separated by a `\n` */
  toInsert: string | string[];
  /** A tag that will be used to keep track of which `expo-plugin-config` introduced the modification */
  tag: string;
  /** The symbol(s) to be used to begin a comment in the given `content`. If an array, the first item will be used to start the comment, the second to end it */
  commentSymbol: string | [string, string];
};

const createCommentSymbols = (commentSymbol: InsertProps['commentSymbol']) => {
  return {
    startCommentSymbol: Array.isArray(commentSymbol) ? commentSymbol[0] : commentSymbol,
    endCommentSymbol: Array.isArray(commentSymbol) ? ` ${commentSymbol[1]}` : '',
  };
};

const createStartTag = (
  commentSymbol: InsertProps['commentSymbol'],
  tag: InsertProps['tag'],
  _: InsertProps['toInsert'],
) => {
  const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
  return `${startCommentSymbol} @generated begin ${tag} - expo prebuild (DO NOT MODIFY)${endCommentSymbol}`;
};

const createEndTag = (commentSymbol: InsertProps['commentSymbol'], tag: InsertProps['tag']) => {
  const { startCommentSymbol, endCommentSymbol } = createCommentSymbols(commentSymbol);
  return `${startCommentSymbol} @generated end ${tag}${endCommentSymbol}`;
};

const createContentToInsert = (
  commentSymbol: InsertProps['commentSymbol'],
  tag: InsertProps['tag'],
  toInsert: InsertProps['toInsert'],
) => {
  const startTag = createStartTag(commentSymbol, tag, toInsert);
  const endTag = createEndTag(commentSymbol, tag);
  return `${startTag}\n${Array.isArray(toInsert) ? toInsert.join('\n') : toInsert}\n${endTag}`;
};

const insert = ({
  content,
  toFind,
  toInsert,
  tag,
  commentSymbol,
  where,
}: InsertProps & {
  where: 'before' | 'after' | 'replace';
}): string => {
  const toInsertWithComments = createContentToInsert(commentSymbol, tag, toInsert);
  if (!content.includes(toFind)) {
    throw new Error(`Couldn't find ${toFind} in the given props.content`);
  }
  if (!content.includes(toInsertWithComments)) {
    switch (where) {
      case 'before':
        content = content.replace(toFind, `${toInsertWithComments}\n${toFind}`);
        break;
      case 'after':
        content = content.replace(toFind, `${toFind}\n${toInsertWithComments}`);
        break;
      case 'replace':
        content = content.replace(toFind, `${toInsertWithComments}`);
        break;
    }
  }
  return content;
};

/**
 * Insert `props.toInsert` into `props.content` the line after `props.toFind`
 * @returns the modified `props.content`
 */
export const insertAfter = (props: InsertProps) => {
  return insert({ ...props, where: 'after' });
};

/**
 * Insert `props.toInsert` into `props.content` the line before `props.toFind`
 * @returns the modified `props.content`
 */
export const insertBefore = (props: InsertProps) => {
  return insert({ ...props, where: 'before' });
};

/**
 * Replace `props.toFind` by `props.toInsert` into `props.content`
 * @returns the modified `props.content`
 */
export const replace = (props: InsertProps) => {
  return insert({ ...props, where: 'replace' });
};

/** Copies `srcFile` to `destFolder` with an optional `destFileName` or its initial name if not provided
 * @returns the path of the created file
 */
const copyFile = async (srcFile: string, destFolder: string, packageName: string) => {
  const fileName = path.basename(srcFile);
  await fs.mkdir(destFolder, { recursive: true });
  const destFile = path.resolve(destFolder, fileName);
  const buf = await fs.readFile(srcFile);
  const content = buf.toString().replace('{pkg}', packageName);
  await fs.writeFile(destFile, content);
  return destFile;
};

const withReactNativeHealthConnect: ConfigPlugin = (config) => {
  config = withAndroidManifest(config, async (config) => {
    const androidManifest = config.modResults.manifest;
    if (!androidManifest?.application?.[0]) {
      throw new Error('AndroidManifest.xml is not valid!');
    }
    if (!androidManifest.application[0]['activity']) {
      throw new Error('AndroidManifest.xml is missing application activity');
    }

    // for Android 13
    androidManifest.application[0]['activity'].push({
      $: {
        'android:name': '.PermissionsRationaleActivity',
        'android:exported': 'true',
      },
      'intent-filter': [
        {
          action: [{ $: { 'android:name': 'androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE' } }],
        },
      ],
    });

    // for Android 14
    // @ts-expect-error activity-alias is not defined in the type
    if (!androidManifest.application[0]['activity-alias']) {
      // @ts-expect-error activity-alias is not defined in the type
      androidManifest.application[0]['activity-alias'] = [];
    }
    // @ts-expect-error activity-alias is not defined in the type
    androidManifest.application[0]['activity-alias'].push({
      $: {
        'android:name': 'ViewPermissionUsageActivity',
        'android:exported': 'true',
        'android:targetActivity': '.PermissionsRationaleActivity',
        'android:permission': 'android.permission.START_VIEW_PERMISSION_USAGE',
      },
      'intent-filter': [
        {
          action: [{ $: { 'android:name': 'android.intent.action.VIEW_PERMISSION_USAGE' } }],
          category: [{ $: { 'android:name': 'android.intent.category.HEALTH_PERMISSIONS' } }],
        },
      ],
    });

    return config;
  });

  config = withMainActivity(config, async (config) => {
    config.modResults.contents = insertAfter({
      content: config.modResults.contents,
      toFind: 'import com.facebook.react.defaults.DefaultReactActivityDelegate',
      toInsert: 'import dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate',
      commentSymbol: '//',
      tag: 'withReactNativeHealthConnect',
    });
    config.modResults.contents = replace({
      content: config.modResults.contents,
      toFind: 'super.onCreate(null)',
      toInsert: ['super.onCreate(savedInstanceState)', 'HealthConnectPermissionDelegate.setPermissionDelegate(this)'],
      commentSymbol: '//',
      tag: 'withReactNativeHealthConnect',
    });
    return config;
  });

  config = withDangerousMod(config, [
    'android',
    async (config) => {
      const pkg = config.android?.package;
      if (!pkg) {
        throw new Error('no package id');
      }
      const projectRoot = config.modRequest.projectRoot;
      const destPath = path.resolve(projectRoot, `android/app/src/main/java/${pkg.split('.').join('/')}`);
      await copyFile(__dirname + '/PermissionRationaleActivity.kt', destPath, pkg);
      return config;
    },
  ]);

  return config;
};

// eslint-disable-next-line import/no-default-export
export default withReactNativeHealthConnect;

Put PermissionRationaleActivity.kt next to the plugin file to work fine. {pkg} will be automatically replaced by the plugin.

package {pkg}

import android.os.Bundle
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity

class PermissionsRationaleActivity: AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val webView = WebView(this)
    webView.webViewClient = object : WebViewClient() {
      override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
        return false
      }
    }

    webView.loadUrl("https://developer.android.com/health-and-fitness/guides/health-connect/develop/get-started")

    setContentView(webView)
  }
}

@matinzd
Copy link
Owner

matinzd commented Mar 18, 2024

Side note here:

Make sure to customise the rationale activity based on your need. That file is just an example webview and you need to create your own native or js view.

@matinzd
Copy link
Owner

matinzd commented Jun 29, 2024

Can someone try this new expo adapter and let me know if it works properly for you?

https://github.com/matinzd/expo-health-connect

@matinzd matinzd linked a pull request Jun 29, 2024 that will close this issue
@ytakzk
Copy link

ytakzk commented Jul 6, 2024

@matinzd I am still encountering the same issue with expo-health-connect.

@matinzd
Copy link
Owner

matinzd commented Jul 6, 2024

Can you specify what kind of issue do you have and provide a log or something?

@ytakzk
Copy link

ytakzk commented Jul 6, 2024

@matinzd Thank you for your quick reply! I am getting the same error as mentioned in the initial post on Android 10 (I haven't tried it on a different device yet).

Here are the dependencies:

Error:

kotlin.UninitializedPropertyAccessException: lateinit property requestPermission has not been initialized
  dev.matinzd.healthconnect.permissions.HealthConnectPermissionDelegate.launch(HealthConnectPermissionDelegate.kt:32)
  dev.matinzd.healthconnect.HealthConnectManager$requestPermission$1$1.invokeSuspend(HealthConnectManager.kt:64)
  kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
  kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
  kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
  kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
  kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
  kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
  kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
  kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)

Let me know if you need further information on how to reproduce this.

@matinzd
Copy link
Owner

matinzd commented Jul 6, 2024

Did you rebuild the project with expo prebuild --clean and added expo-health-connect under plugins in app.json?
Can you also try it on newer Android versions?
Do you have the same problem on Android 10 when you run this example app?

@ytakzk
Copy link

ytakzk commented Jul 9, 2024

@matinzd After closely comparing your example with mine, I discovered that expo-dev-client was somehow interfering with the behavior of expo-health-connect. Once I removed it from package.json, everything started working without errors.
Thank you very much!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
7 participants