33

I'm trying to use the url_launcher plugin to open youtube videos by link but the canLaunch function keeps throwing an error. I'm able to bypass this error only by completely removing the canLaunch function but can't figure out what is wrong.

Code not working:

_goToVideo(YoutubeVideoData video) async {
  if (await canLaunch(video.url)) {
    await launch(video.url);
  } else {
    throw 'Could not launch ${video.url}';
  }
}

Code working:

_goToVideo(YoutubeVideoData video) async {
  await launch(video.url);
}

I'm not quite sure why I can't use the canLaunch method as written in the README Example

Error:

E/flutter (12574): [ERROR:flutter/lib/ui/ui_dart_state.cc(166)] Unhandled Exception: Could not launch https://www.youtube.com/watch?v=-3g5WlqJtIo
E/flutter (12574): #0      _goToVideo (package:esfandapp/widgets/newsList/videoCard.dart:71:5)
E/flutter (12574): <asynchronous suspension>
E/flutter (12574): #1      VideoCard.build.<anonymous closure> (package:esfandapp/widgets/newsList/videoCard.dart:13:20)
E/flutter (12574): #2      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:992:19)
E/flutter (12574): #3      _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:1098:38)
E/flutter (12574): #4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:184:24)
E/flutter (12574): #5      TapGestureRecognizer.handleTapUp (package:flutter/src/gestures/tap.dart:524:11)
E/flutter (12574): #6      BaseTapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:284:5)
E/flutter (12574): #7      BaseTapGestureRecognizer.acceptGesture (package:flutter/src/gestures/tap.dart:256:7)
E/flutter (12574): #8      GestureArenaManager.sweep (package:flutter/src/gestures/arena.dart:158:27)
E/flutter (12574): #9      GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:224:20)
E/flutter (12574): #10     GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:200:22)
E/flutter (12574): #11     GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:158:7)
E/flutter (12574): #12     GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:104:7)
E/flutter (12574): #13     GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:88:7)
E/flutter (12574): #14     _rootRunUnary (dart:async/zone.dart:1206:13)
E/flutter (12574): #15     _CustomZone.runUnary (dart:async/zone.dart:1100:19)
E/flutter (12574): #16     _CustomZone.runUnaryGuarded (dart:async/zone.dart:1005:7)
E/flutter (12574): #17     _invoke1 (dart:ui/hooks.dart:267:10)
E/flutter (12574): #18     _dispatchPointerDataPacket (dart:ui/hooks.dart:176:5)

Widget using the function:

class VideoCard extends StatelessWidget {
  final YoutubeVideoData video;
  VideoCard({this.video});

  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () => _goToVideo(video),
      child: Container(
        child: Card(
          child: Container(
            child: Column(
              children: [
                Align(
                  child: Padding(
                    child: Text(
                      video.title,
                      style: TextStyle(
                        fontFamily: 'Roboto Condensed',
                        fontSize: 16,
                      ),
                    ),
                    padding: EdgeInsets.fromLTRB(15, 0, 15, 10),
                  ),
                  alignment: Alignment.centerLeft,
                ),
                Container(
                    child: Image.network(video.thumbnails[1], fit: BoxFit.cover,),
                  width: MediaQuery.of(context).size.width,
                ),
                Align(
                  child: Container(
                    child: Text(
                      video.date.toString() + "",
                      style: TextStyle(
                        fontFamily: 'Roboto Condensed',
                        fontSize: 14,
                        fontWeight: FontWeight.w300,
                      ),
                    ),
                    padding: EdgeInsets.fromLTRB(15, 5, 15, 0),
                  ),
                  alignment: Alignment.centerLeft,
                ),
              ],
            ),
            width: MediaQuery.of(context).size.width - 32,
            padding: EdgeInsets.symmetric(
              horizontal: 0,
              vertical: 10,
            ),
            alignment: Alignment.center,
          ),
          shape: RoundedRectangleBorder(
              borderRadius: BorderRadius.all(Radius.circular(25))),
        ),
      ),
    );
  }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Nikcio
  • 407
  • 1
  • 5
  • 10

13 Answers13

78

Starting from API30 (Android 11), your Android app has to list all apps it interacts with.

You can add:

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

in your android manifest to bypass it or specifically list them.

For more info: https://developer.android.com/about/versions/11/privacy/package-visibility

Johny B
  • 944
  • 8
  • 15
  • 11
    Be very careful with permission "QUERY_ALL_PACKAGES". Google Play is starting to restrict it on May 5, 2021 and you app may be removed from the store: https://support.google.com/googleplay/android-developer/answer/10158779 – Valentin Apr 06 '21 at 07:22
72

Personally, I don't like the uncertainty that seems to come with using the QUERY_ALL_PACKAGES permission (because Google could stop letting people use it in the future). For that reason, I did some investigation and found that adding the following to my AndroidManifest.xml allows my app to open the browser, phone app and email app on API 30:

<manifest>

    <!-- Nest within the manifest element, not the application element-->
    <queries>
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <intent>
            <action android:name="android.intent.action.DIAL" />
            <data android:scheme="tel" />
        </intent>
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="*/*" />
        </intent>
    </queries>

    <application>
        ....
    </application>
</manifest>

To get the same functionality working on iOS, it may be necessary to add the following to your info.plist file:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
    <string>tel</string>
    <string>mailto</string>
</array> 

Just wanted to share in case it helps someone else out.

Wes1324
  • 1,077
  • 8
  • 13
  • Thank you. My Android app didn't work without these settings. Do we have such a setting for iOS? I am testing on iPhone 12 Pro max emulator. My URLs can be opened but not Phone and email. So, I am wondering is there such a setting for iOS, too? – Hesam Mar 31 '21 at 17:27
  • 1
    No problem @Hesam, I have updated my answer with iOS-specific info – Wes1324 Apr 01 '21 at 06:31
  • thanks for updating your answer. I added those things but phone and email still don't work on the emulator although it works on the actual device. I am not an iOS developer so I am wondering is it an emulator issue or is a common issue for iOS developers. – Hesam Apr 01 '21 at 16:59
  • 1
    Hi @Hesam, yes I seem to remember from my testing that phone calls and email are not supported by the Xcode simulator. The following links agree with this: https://discussions.apple.com/thread/3830910 and https://www.dummies.com/web-design-development/mobile-apps/the-ios-simulators-limitations/. Were phone calls and email working on your actual device before you added the LSApplicationQueriesSchemes to your info.plist file? – Wes1324 Apr 02 '21 at 06:45
  • Execution failed for task ':app:processDebugResources'...The process cannot access the file because it is being used by another process. Getting this error after adding the above lives in manifest tag in android. – Apoorv pandey May 06 '21 at 05:57
  • Hi @Apoorvpandey, I've never seen this error before. After doing a bit of research, it seems like one possible cause could be an open file handle on one of the files in your build directory, as per this answer (https://stackoverflow.com/a/35674224/6630965). Please try deleting your build directory and retrying. Let me know if it doesn't work. – Wes1324 May 06 '21 at 22:57
  • Yes, this worked. I had to delete the build and do one or two things here and there in the app. – Apoorv pandey May 07 '21 at 06:14
  • I have a problem that the phone app is launched with the number written but it's not called, I have to click on the dial to make the call. is that what the package should do? – Shalabyer Sep 24 '21 at 21:17
  • @Shalabyer, in the past I remember that clicking on a phone number would cause a call to be made immediately on iOS and Android, but I believe it is now standard behaviour on both platforms to just open the phone app with the number (probably so that you users don't make calls by mistake). I tested this on both an Android and iOS phone by searching for a local restaurant on Google and pressing the 'Call' button. On both iOS and Android, I experienced the behaviour that you describe. So I believe this behaviour is controlled by the OS, not the package. – Wes1324 Sep 25 '21 at 15:57
  • Great I used both go the above but this answer is better because of googles policies, thanks! – flutterloop Nov 05 '21 at 14:49
  • It's better than using QUERY_ALL_PACKAGES for google limitations – Amgad Mahmoud Sep 16 '22 at 04:19
8

Even after trying accepted answer, If its not working for you then Please try followed code.

PRE STEPS: Do as accepted answer suggested.

Add following tags in AndroidManifest.xml before <application/>

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <data android:scheme="https" />
    </intent>
    <intent>
        <action android:name="android.intent.action.DIAL" />
        <data android:scheme="tel" />
    </intent>
    <intent>
        <action android:name="android.intent.action.SEND" />
        <data android:mimeType="*/*" />
    </intent>
</queries>

NOW WHAT I HAVE DONE

Create one method:

Future<void> _makeSocialMediaRequest(String url) async {
    if (await canLaunch(url)) {
      await launch(url);
    } else {
      throw 'Could not launch $url';
    }
  }

and Called it by following way:

  //FOR EMAIL
  final Uri _emailLaunchUri = Uri(
      scheme: 'mailto',
      path: 'pratik13butani@gmail.com',
      queryParameters: {'subject': 'Pratik Butani'});
  _makeSocialMediaRequest(_emailLaunchUri.toString());

  //FOR PHONE NUMBER:
  final Uri _phoneLaunchUri =
      Uri(scheme: 'tel', path: postOffice.mobileNo);
  _makeSocialMediaRequest(_phoneLaunchUri.toString());

  //FOR ANY URL.. YOU CAN PASS DIRECT URL..
  _makeSocialMediaRequest("http://pratikbutani.com");

Its working for me. Hopefully it will work for you too. Thank you.

Pratik Butani
  • 60,504
  • 58
  • 273
  • 437
  • I have a problem that the phone app is launched with the number written but it's not called, I have to click on the dial to make the call. is that what the package should do? – Shalabyer Sep 24 '21 at 21:16
7

bro only put '!' before "if (!await canLaunch(url))" Use this -->

 if (!await canLaunch(url)){
  await launch(
    url,
    forceSafariVC: false,
    forceWebView: false,
    headers: <String, String>{'my_header_key': 'my_header_value'},
  );
} else {
  
  throw 'Could not launch $url';
}
4

For Android Use the below code in Android/main/res/AndroidManifest.xml like the showed in the image

enter image description here

and here is the code

<intent>
    <action android:name="android.intent.action.VIEW" />
    <data android:scheme="https" />
</intent>
<intent>
    <action android:name="android.intent.action.DIAL" />
    <data android:scheme="tel" />
</intent>
<intent>
    <action android:name="android.intent.action.SEND" />
    <data android:mimeType="*/*" />
</intent>

For IPhone ios/Runner/Info.plist like the showed in the image

enter image description here

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
    <string>tel</string>
    <string>mailto</string>
3

Make sure to add in the android manifest the below line

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

Don't forget about the Info.plist as well

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
    <string>tel</string>
    <string>mailto</string>
</array> 
Imran Sefat
  • 528
  • 4
  • 6
1

I spent nearly 2 hours to find what is wrong and after I closed the application from the recent tab and re-run the app it worked perfectly fine.

Things you might missed,

  1. adding Url_launcher dependency in pubspec.yaml
  2. Adding queries in Android manifest file after tag

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
</queries>
  1. Adding keys in the info.plist file under ios directory

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
</array> 
  1. Just try to uninstall app and run the code
1
 if (await canLaunchUrl(Uri.parse(url))) {
      await launchUrl(Uri.parse(url),
      mode: LaunchMode.externalApplication, );
    } 
0

i faced same error using win and linux for android developments and solve it with these steps the idea is to add the to AndroidManifest.xml in app/src/main/AndroidManifest.xml to do so and to make it work without error you must update the gardle version so here is the steps 1-updated my gradle-wrapper.properties to gradle-6.7.1-all.zip

distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip

2- updated my classpath in android/build.gradle to com.android.tools.build:gradle:4.2.1

buildscript {
    ext.kotlin_version = '1.3.50'
    repositories {
        google()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:4.2.1'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

rootProject.buildDir = '../build'
subprojects {
    project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
    project.evaluationDependsOn(':app')
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

3- add the above to my AndroidManifest.xml in app/src/main/AndroidManifest.xml

   <queries>
        <!-- If your app opens https URLs -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <!-- If your app makes calls -->
        <intent>
            <action android:name="android.intent.action.DIAL" />
            <data android:scheme="tel" />
        </intent>
        <!-- If your app emails -->
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="*/*" />
        </intent>
    </queries>

and now everything work fine

0

If none of the above works, try adding this in the AndroidManifest.xml (main one if you have that). Add this code snipped just above the <application> tag.

<queries>
    <intent>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="https" />
    </intent>
</queries>
0

For Android Just add in your androidmanifest.xml file

<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>

For iOS add these in your info.plist file

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>https</string>
    <string>http</string>
    <string>tel</string>
    <string>mailto</string>
</array> 
Anand
  • 4,355
  • 2
  • 35
  • 45
0

QUERY_ALL_PACKAGES (not recommended)

Because Google could stop letting people use it in the future as mentioned in the official document

if you use launch any url from your app just use

<manifest package="com.example.game">
    <queries>
        <package android:name="com.android.chrome" />
        
    </queries>
    ...
</manifest>

likewise you can add <queries> depends on your need.

ex.

<queries>
        <!-- If your app opens https URLs -->
        <intent>
            <action android:name="android.intent.action.VIEW" />
            <data android:scheme="https" />
        </intent>
        <!-- If your app makes calls -->
        <intent>
            <action android:name="android.intent.action.DIAL" />
            <data android:scheme="tel" />
        </intent>
        <!-- If your app emails -->
        <intent>
            <action android:name="android.intent.action.SEND" />
            <data android:mimeType="*/*" />
        </intent>
    </queries>
Anand
  • 4,355
  • 2
  • 35
  • 45
-4

This worked for me

if (!url.contains('http')) url = 'https://$url';

Complete Method:

launchURL(String url) async {
  if (!url.contains('http')) url = 'https://$url';
  if (await canLaunch(url)) {
    await launch(url);
  } else {
    throw 'Could not launch $url';
  }
}
mohmdezeldeen
  • 396
  • 1
  • 3
  • 11