-
Go to Project -> Targets -> Runner -> Info tab
-
Add
Application supports iTunes file sharing
with valuetrue
to the key value table or info.plist file -
Add
Supports opening documents in place
with valuetrue
to the key value table or info.plist file -
Add information about the extension to the
Exported Type Identifiers
Tab.- Description: Any description e.g. ULPT Performance Data
- Identifier: Any identifier that is not reserved. E.g. org.frenchtacodev.ulpt
- Conforms to: public.data, public.content (content is optional)
- Extensions: ulpt
-
Add information about the extension to the
Document Types
Tab.- Name: Any name e.g. ULPT Performance Data
- Types: Same as identifier e.g. org.frenchtacodev.ulpt
- Handler Rank: Owner
<plist version="1.0">
<dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>ULPT Performance Data</string>
<key>LSHandlerRank</key>
<string>Owner</string>
<key>LSItemContentTypes</key>
<array>
<string>org.frenchtacodev.ulpt</string>
</array>
</dict>
</array>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>UTExportedTypeDeclarations</key>
<array>
<dict>
<key>UTTypeConformsTo</key>
<array>
<string>public.data</string>
<string>public.content</string>
</array>
<key>UTTypeDescription</key>
<string>ULPT Performance Data</string>
<key>UTTypeIconFiles</key>
<array/>
<key>UTTypeIdentifier</key>
<string>org.frenchtacodev.ulpt</string>
<key>UTTypeTagSpecification</key>
<dict>
<key>public.filename-extension</key>
<array>
<string>ulpt</string>
</array>
<key>public.mime-type</key>
<array>
<string>application/example</string>
</array>
</dict>
</dict>
</array>
</dict>
</plist>
Add the following code to the AppDelegate.swift
file in the Flutter Project:
Define this variable within the file:
private var commChannel: FlutterMethodChannel? = nil;
Add this to the override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
//GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController;
commChannel = FlutterMethodChannel(name: "nativeCommChannel", binaryMessenger: controller.binaryMessenger);
//return super.application(application, didFinishLaunchingWithOptions: launchOptions)
Override this funtion within the file:
override func application(_ app: UIApplication, open url: URL, options:[UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
if(commChannel == nil){return true;}
var copyUrl: URL?;
var copyUrlPath: String?;
if #available(iOS 14.0, *) {
copyUrl = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent, conformingTo: UTType.data);
} else {
copyUrl = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent);
};
if #available(iOS 16, *){
copyUrlPath = copyUrl?.path();
}else{
copyUrlPath = copyUrl?.absoluteString;
}
if(copyUrl == nil || copyUrlPath == nil){return true;}
do{
if(FileManager.default.fileExists(atPath: copyUrlPath!)){
try FileManager.default.removeItem(at: copyUrl!);
}
//If false, access is not granted
if(url.startAccessingSecurityScopedResource() == false){return true;}
try FileManager.default.copyItem(at: url, to: copyUrl!);
url.stopAccessingSecurityScopedResource();
}catch{
url.stopAccessingSecurityScopedResource();
return true;
}
commChannel!.invokeMethod("onArgsFromNative", arguments: copyUrlPath!);
return true;
}
File importing is then handled by ULPT.
To support file opening on windows with the "Right click -> Open with" functionality, the following steps are needed:
- Go into the projects windows folder. This is done on the app site and not in the package. E.g. in this project within the example folder/project.
- The APIEntry wWinMain in the ´main.cpp´ file of the project contains a wchar_t pointer called command_line. We need this information
- open flutter_window.h and add under private:
private:
// The project to run.
flutter::DartProject project_;
// The Flutter instance hosted by this window.
std::unique_ptr<flutter::FlutterViewController> flutter_controller_;
//Arguments passed on starting the win32 App
std::string args_;
//Utility function to convert the args into flutter encodable value.
std::string ConvertArgsToString(const wchar_t* args);
- change the constructor to:
explicit FlutterWindow(const flutter::DartProject& project, const wchar_t* args);
- open flutter_window.cpp and add the following function implementation:
//Converts the arguments passed in from the command line that are represented as a wchar_t pointer.
//Since flutter can only handle std::strings in method communication, conversion is needed.
std::string FlutterWindow::ConvertArgsToString(const wchar_t* args)
{
std::string result = std::string();
if (args == nullptr) return result;
int length = WideCharToMultiByte(CP_UTF8, 0, args, -1, nullptr, 0, nullptr, nullptr);
char* buffer = new char[length];
WideCharToMultiByte(CP_UTF8, 0, args, -1, buffer, length, nullptr, nullptr);
result = std::string(buffer);
delete[] buffer;
return result;
}
- modify the constructor implemenation this way:
FlutterWindow::FlutterWindow(const flutter::DartProject& project, const wchar_t* args) : project_(project){
args_ = ConvertArgsToString(args);
}
- in the
OnCreate
function add the following code after theForceRedraw()
call:
//Create and Invoke the Communication Channel to the flutter app letting it know about arguments if there are any
if (args_ == "") return true;
flutter::MethodChannel channel(flutter_controller_->engine()->messenger(), "nativeCommChannel", &flutter::StandardMethodCodec::GetInstance());
channel.InvokeMethod(std::string("onArgsFromNative"), std::make_unique<flutter::EncodableValue>(flutter::EncodableValue(args_)));
- in
main.cpp
function exchangeFlutterWindow window(project);
forFlutterWindow window(project, command_line);
to call the constructor with the command line arguments.
Now when ULPT is opened from a file, the c++ side provides the path and the import should work.
Go into your apps AndroidManifest.xml and add the following lines under the MainActivities Activity Tab:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data
android:scheme="file"
android:host="*"
android:pathPattern=".*\\.ulpt" />
</intent-filter>
<intent-filter android:priority="1">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.OPENABLE" />
<data android:host="*" />
<data android:mimeType="application/octet-stream" />
<data android:pathPattern=".*\\..*\\..*\\..*\\..*\\.ulpt" />
<data android:pathPattern=".*\\..*\\..*\\..*\\.ulpt" />
<data android:pathPattern=".*\\..*\\..*\\.ulpt" />
<data android:pathPattern=".*\\..*\\.ulpt" />
<data android:pathPattern=".*\\.ulpt" />
<data android:scheme="content" />
</intent-filter>
Add the following code to the MainActivity.java
file in the Flutter Project (Android Side):
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//Opens up flutters method channel to communicate with the dart side
MethodChannel methodChannel = new MethodChannel(
Objects.requireNonNull(this.getFlutterEngine()).getDartExecutor().getBinaryMessenger(),
"nativeCommChannel"
);
Intent intent = getIntent();
if(intent == null) return;
String cachePath = handlePerfFileIntent(intent);
if(cachePath == null) return;
//Send the cached filepath to the dart side
methodChannel.invokeMethod("onArgsFromNative", cachePath);
}
//This function takes the content intent and
//1 reads its data, 2 copies the data to the app cache directory, 3 returns the cached files path.
//This is necessary because we do not have permission to read storage files on android out of the box.
private String handlePerfFileIntent(Intent intent){
Uri uri = intent.getData();
if(uri == null) return null;
String path = getCacheDir() + "/" + "tempPerfData.ulpt";
File outputFile = new File(path);
if(outputFile.exists()) outputFile.delete();
try {
InputStream is = getContentResolver().openInputStream(uri);
if(is == null) return null;
FileOutputStream os = new FileOutputStream(outputFile, false);
byte[] buffer = new byte[4096];
int b;
while((b = is.read(buffer)) != -1){
os.write(buffer, 0, b);
}
os.close();
is.close();
} catch (IOException e) {
return null;
}
return path;
}