{*******************************************************} { } { Helpers for Android } { } { CopyRight (C) 2018-2020 KngStr } { } { Some Code from } { Kastri Free of DelphiWorlds } { QDAC of swish } { Thanks } { } {*******************************************************} unit ksAndroid.Helpers; interface uses System.SysUtils, Androidapi.JNIBridge, Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.App, Androidapi.JNI.JavaTypes, Androidapi.JNI.Net; type TAndroidHelperEx = record private class function GetJActivity: JActivity; static; class function GetJContext: JContext; static; class function GetJActivityManager: JActivityManager; static; public const ICE_CREAM_SANDWICH = 14; ICE_CREAM_SANDWICH_MR1 = 15; JELLY_BEAN = 16; JELLY_BEAN_MR1 = 17; JELLY_BEAN_MR2 = 18; KITKAT = 19; KITKAT_MR1 = 20; LOLLIPOP = 21; LOLLIPOP_MR1 = 22; MARSHMALLOW = 23; NOUGAT = 24; NOUGAT_MR1 = 25; OREO = 26; OREO_MR1 = 27; PIE = 28; Q = 29; /// /// Checks if both build and target are greater or equal to the tested value /// class function CheckBuildAndTarget(const AValue: Integer): Boolean; static; /// /// Returns the equivalent of "AndroidClass.class" /// class function GetClass(const APackageClassName: string): Jlang_Class; static; /// /// Returns the application default icon ID /// class function GetDefaultIconID: Integer; static; /// /// Returns a URI to the notification sound /// class function GetDefaultNotificationSound: Jnet_Uri; static; /// /// Returns target Sdk version /// class function GetTargetSdkVersion: Integer; static; /// /// Returns installed Sdk version /// class function GetBuildSdkVersion: Integer; static; /// /// Returns whether the activity is running foreground /// /// /// Useful from within a service to determine whether or not the service needs to run in foreground mode /// class function IsActivityForeground: Boolean; static; /// /// Get column data from uri /// class function GetColumnAsString(const AUri: Jnet_Uri; const AColumn: JString; ASelection: JString = nil; ASelectionArgs: TJavaObjectArray = nil; ASortOrder: JString = nil): string; static; /// /// Get data column data from uri /// class function GetDataColumnAsString(const AUri: Jnet_Uri; ASelection: JString = nil; ASelectionArgs: TJavaObjectArray = nil; ASortOrder: JString = nil): string; static; /// /// Converts uri to file path /// class function FileFromUri(const AUri: Jnet_Uri): string; overload; static; /// /// Converts file to uri, without FileProvider /// class function UriFromFile(const AFile: JFile): Jnet_Uri; overload; static; /// /// Converts file to uri, without FileProvider /// class function UriFromFile(const AFileName: string): Jnet_Uri; overload; static; /// /// Converts file to uri, using FileProvider if target API >= 24 /// /// /// Use this only when accessing files with an "external" URI /// class function SharedUriFromFile(const AFile: JFile; const AAuthority: string = ''): Jnet_Uri; overload; static; /// /// Converts filename to uri, using FileProvider if target API >= 24 /// /// /// Use this only when accessing files with an "external" URI /// class function SharedUriFromFile(const AFileName: string; const AAuthority: string = ''): Jnet_Uri; overload; static; /// /// TJnet_Uri.JavaClass.parse /// class function UriParse(const S: string): Jnet_Uri; overload; static; /// /// TJnet_Uri.JavaClass.parse /// class function UriParse(const S: JString): Jnet_Uri; overload; static; /// Returns Java Application Context class property Context: JContext read GetJContext; /// Returns Java Application Activity /// An exception will be launched if there is no activity, for example a Service class property Activity: JActivity read GetJActivity; /// Returns Java Application Activity Manager class property ActivityManager: JActivityManager read GetJActivityManager; /// Need reorder tasks permission class procedure BringAppToFront; static; /// Need reorder tasks permission class procedure SendAppToBack; static; /// Call a Java Activity class function StartActivity(Intent: JIntent; const Code: Integer = -1): Boolean; static; /// Check HasAssocApp and Call a Java Activity class function CheckAndStartActivity(Intent: JIntent; const Code: Integer = -1): Boolean; static; /// Returns Application package name class function GetPackageName: string; static; /// Returns Application package label class function GetPackageLabel: string; static; /// Returns MimeType from filename class function GetMimeType(AFileName: string): JString; static; /// Returns Primary SDCard path class function GetSDCardPath: string; static; /// Checks if there is at least one application capable of receiving the intent. class function HasAssocApp(const URI: string): Boolean; overload; static; /// Checks if there is at least one application capable of receiving the intent. class function HasAssocApp(const Intent: JIntent): Boolean; overload; static; /// Install an android package: xxx.apk /// /// /// class function InstallPackage(const AFileName: string; const AAuthority: string = ''): Boolean; static; /// Check if an application is installed class function IsAppInstalled(const APackage: string): Boolean; static; /// Add/Cear FLAG_KEEP_SCREEN_ON class function KeepScreen(AOn: Boolean): Boolean; static; end; implementation uses {$IFDEF DEBUG}FMX.Types,{$ENDIF} Androidapi.JNI.Os, Androidapi.JNI.Support, Androidapi.JNI.Media, Androidapi.JNI.Provider, Androidapi.JNI.Webkit, Androidapi.Helpers, FMX.Helpers.Android, System.IOUtils; { TAndroidHelperEx } class procedure TAndroidHelperEx.BringAppToFront; begin ActivityManager.moveTaskToFront(Activity.getTaskId, TJIntent.JavaClass.FLAG_ACTIVITY_NEW_TASK); end; class function TAndroidHelperEx.CheckAndStartActivity(Intent: JIntent; const Code: Integer): Boolean; begin Result := (Intent <> nil) and HasAssocApp(Intent); if not Result then Exit; Result := TAndroidHelperEx.StartActivity(Intent, Code); end; class function TAndroidHelperEx.CheckBuildAndTarget(const AValue: Integer): Boolean; begin Result := (GetBuildSdkVersion >= AValue) and (GetTargetSdkVersion >= AValue); end; (* * >=4.4 * uri=content://com.android.providers.media.documents/document/image%3A293502 * uri=file:///storage/emulated/0/temp_photo.jpg * uri=content://media/external/images/media/193968 * <4.4 * uri=content://media/external/images/media/13 * third party * content://com.speedsoftware.explorer.fileprovider/root/storage/emulated/0/Android/data/com.lifan.qspsy/files/cache/thumb/F6AB021A6BBCEFC3B942625FBA2E6ADE/7.jpg * content://com.tencent.mtt.fileprovider/QQBrowser/Movies/BVR_2019_10_14_10_52_29_trimq.mp4 * content://com.estrongs.files/storage/emulated/0/DCIM/360%E8%A1%8C%E8%BD%A6%E8%AE%B0%E5%BD%95%E4%BB%AA/2018_02_11_14_16_40_ABAC1BB6.mp4 * * 参考文献 * https://github.com/coltoscosmin/FileUtils/blob/master/FileUtils.java * https://github.com/DB-BOY/FileChoose/blob/master/app/src/main/java/cn/dbboy/filechoose/FileUtil.java * https://www.jianshu.com/p/c5f207f8cce6 * https://blog.csdn.net/chengfu116/article/details/74923161 * https://www.cnblogs.com/epmouse/p/5421048.html * * 暂未实现 * 安卓10好像完全没有获取真实路径的机会 * 安卓7/8/9貌似有些机型还是不行,只能是获取内容后在自己可读写的区域存一份儿 * 如果是获取内容而不是path的话,就不需要用这里了,单独搞个api更好 *) class function TAndroidHelperEx.FileFromUri(const AUri: Jnet_Uri): string; function GetUriByType(S: string): Jnet_Uri; begin if SameText(S, 'image') then Result := TJImages_Media.JavaClass.EXTERNAL_CONTENT_URI else if SameText(S, 'video') then Result := TJVideo_Media.JavaClass.EXTERNAL_CONTENT_URI else if SameText(S, 'audio') then Result := TJAudio_Media.JavaClass.EXTERNAL_CONTENT_URI else Result := nil; end; function GetDocId(var ADocId, AType, AId: string): Boolean; var LJString: JString; LArr: TArray; begin Result := False; LJString := TJDocumentsContract.JavaClass.getDocumentId(AUri); if LJString = nil then Exit; ADocId := JStringToString(LJString); if ADocId = '' then Exit; LArr := ADocId.Split([':']); if Length(LArr) < 2 then begin AType := ''; AId := ADocId; end else begin AType := LArr[0]; AId := LArr[1]; end; Result := AId <> ''; end; var LSdCard, LPath: string; LAuthority, LScheme, LDocId, LType, LId: string; LSelection: JString; LUri: Jnet_Uri; LArr: TArray; I: Integer; iId: Int64; begin Result := ''; if AUri = nil then Exit; LSdCard := GetSDCardPath; LAuthority := JStringToString(AUri.getAuthority().toString); LScheme := JStringToString(AUri.getScheme()); {$IFDEF DEBUG} Log.d('---TAndroidHelperEx.FileFromUri-AUri:%s', [JStringToString(AUri.toString)]); Log.d('---TAndroidHelperEx.FileFromUri-AUri.Authority:%s-LScheme:%s', [LAuthority, LScheme]); {$ENDIF} if CheckBuildAndTarget(KITKAT) and TJDocumentsContract.JavaClass.isDocumentUri(Context, AUri) then begin if not GetDocId(LDocId, LType, LId) then Exit; {$IFDEF DEBUG} Log.d('---TAndroidHelperEx.FileFromUri-LDocId:%s-LType:%s-LId:%s', [LDocId, LType, LId]); {$ENDIF} if SameText(LAuthority, 'com.android.externalstorage.documents') then begin if SameText(LType, 'primary') then Result := TPath.Combine(LSdCard, LId) else if SameText(LType, 'home') then Result := TPath.Combine(TPath.GetSharedDocumentsPath, LId) {$IFDEF DEBUG} else Log.d('---TAndroidHelperEx.FileFromUri-Unkown externalstorage-LDocId:%s-LType:%s-LId:%s', [LDocId, LType, LId]); {$ENDIF} end else if SameText(LAuthority, 'com.android.providers.media.documents') then begin LSelection := StringToJString('_id=' + LId); LUri := GetUriByType(LType); if LUri <> nil then Result := GetDataColumnAsString(LUri, LSelection); end else if SameText(LAuthority, 'com.android.providers.downloads.documents') then begin if SameText(LType, 'raw') then Result := Copy(LDocId, 5) //else if GetBuildSdkVersion < OREO then begin // 有人说O的时候不需要自己解析,但测试锤子手机7.1,不行 else begin SetLength(LArr, 2); LArr[0] := 'content://downloads/public_downloads'; LArr[1] := 'content://downloads/my_downloads'; //LArr[2] := 'content://downloads/all_downloads'; // 这个貌似没权限 iId := StrToInt64Def(LId, -1); for I := 0 to Length(LArr) - 1 do begin LUri := TJContentUris.JavaClass.withAppendedId(UriParse(LArr[I]), iId); if LUri <> nil then try Result := GetDataColumnAsString(LUri); if Result <> '' then Break; except on E: Exception do begin {$IFDEF DEBUG} Log.d('---TAndroidHelperEx.FileFromUri Error: [%s]%s', [E.ClassName, E.Message]); {$ENDIF} end; end; end; end; //else if Result = '' then Result := GetDataColumnAsString(AUri); end {$IFDEF DEBUG} else Log.d('---TAndroidHelperEx.FileFromUri-Unkown DocumentUri-'); {$ENDIF} end else if SameText(LScheme, 'content') then begin // Return the remote address if SameText(LAuthority, 'com.google.android.apps.photos.content') then Result := JStringToString(AUri.getLastPathSegment()) else if SameText(LAuthority, 'com.tencent.mtt.fileprovider') then begin LPath := JStringToString(AUri.getPath()); if Pos('/QQBrowser/', LPath) = 1 then begin // /QQBrowser/XXX LPath := TPath.Combine(LSdCard, Copy(LPath, 12)); if FileExists(LPath) then Result := LPath; end; end else if SameText(LAuthority, 'com.speedsoftware.explorer.fileprovider') then begin LPath := JStringToString(AUri.getPath()); if Pos('/root/', LPath) = 1 then Result := Copy(LPath, 6); end else if SameText(LAuthority, 'com.estrongs.files') then Result := JStringToString(AUri.getPath()) else Result := GetDataColumnAsString(AUri); end else if SameText(LScheme, 'file') then Result := JStringToString(AUri.getPath()) {$IFDEF DEBUG} else Log.d('---TAndroidHelperEx.FileFromUri-Unkown Uri-'); {$ENDIF} end; class function TAndroidHelperEx.GetBuildSdkVersion: Integer; begin Result := TJBuild_VERSION.JavaClass.SDK_INT; end; class function TAndroidHelperEx.GetClass(const APackageClassName: string): Jlang_Class; begin Result := TJLang_Class.JavaClass.forName(StringToJString(APackageClassName), True, Context.getClassLoader); end; class function TAndroidHelperEx.GetColumnAsString(const AUri: Jnet_Uri; const AColumn: JString; ASelection: JString; ASelectionArgs: TJavaObjectArray; ASortOrder: JString): string; var LCursor: JCursor; LIndex: Integer; LPojection: TJavaObjectArray; begin Result := ''; if AUri = nil then Exit; LPojection := TJavaObjectArray.Create(1); LPojection.Items[0] := AColumn; LCursor := Context.getContentResolver().query(AUri, LPojection, ASelection, ASelectionArgs, ASortOrder); if LCursor = nil then begin {$IFDEF DEBUG} Log.d('---TAndroidHelperEx.GetColumnAsString-LCursor = nil-'); {$ENDIF} Exit; end; try LIndex := LCursor.getColumnIndex(AColumn); if (LIndex > -1) and LCursor.moveToFirst then Result := JStringToString(LCursor.getString(LIndex)) {$IFDEF DEBUG} else Log.d('---TAndroidHelperEx.GetColumnAsString-LIndex:%d-', [LIndex]); {$ENDIF} finally LCursor.close; end; end; class function TAndroidHelperEx.GetDataColumnAsString(const AUri: Jnet_Uri; ASelection: JString; ASelectionArgs: TJavaObjectArray; ASortOrder: JString): string; begin Result := GetColumnAsString(AUri, StringToJString('_data'), ASelection, ASelectionArgs, ASortOrder); end; class function TAndroidHelperEx.GetDefaultIconID: Integer; begin Result := Context.getApplicationInfo.icon; end; class function TAndroidHelperEx.GetDefaultNotificationSound: Jnet_Uri; begin Result := TJRingtoneManager.JavaClass.getDefaultUri(TJRingtoneManager.JavaClass.TYPE_NOTIFICATION); end; class function TAndroidHelperEx.GetJActivity: JActivity; begin Result := {$IF CompilerVersion > 27} TAndroidHelper.Activity {$ELSE} SharedActivity {$ENDIF} end; class function TAndroidHelperEx.GetJActivityManager: JActivityManager; var AService: JObject; begin AService := Context.getSystemService(TJContext.JavaClass.ACTIVITY_SERVICE); if AService <> nil then Result := TJActivityManager.Wrap((AService as ILocalObject).GetObjectID) else Result := nil; end; class function TAndroidHelperEx.GetJContext: JContext; begin Result := {$IF CompilerVersion > 27} TAndroidHelper.Context; {$ELSE} SharedActivityContext; {$ENDIF} end; class function TAndroidHelperEx.GetMimeType(AFileName: string): JString; var LExt: string; LTypeMap: JMimeTypeMap; begin Result := nil; if AFileName = '' then Exit; LExt := LowerCase(ExtractFileExt(AFileName)); if Length(LExt) < 2 then Exit; LExt := Copy(LExt, 1); LTypeMap := TJMimeTypeMap.JavaClass.getSingleton(); if LTypeMap = nil then Exit; Result := LTypeMap.getMimeTypeFromExtension(StringToJString(LExt)); if Result <> nil then Result := Result.trim(); end; class function TAndroidHelperEx.GetPackageLabel: string; var LApplicationInfo: JApplicationInfo; LLabel: JCharSequence; begin Result := ''; try LApplicationInfo := Context.getPackageManager.getApplicationInfo(Context.getPackageName, 0); if LApplicationInfo <> nil then begin LLabel := Context.getPackageManager.getApplicationLabel(LApplicationInfo); if LLabel <> nil then Result := JCharSequenceToStr(LLabel); end; except end; end; class function TAndroidHelperEx.GetPackageName: string; begin Result := JStringToString(Context.getPackageName); end; class function TAndroidHelperEx.GetSDCardPath: string; var sPath: string; begin Result := ''; sPath := System.IOUtils.TPath.GetSharedDocumentsPath; if Pos(PathDelim, sPath) = 0 then Exit; Result := ExtractFilePath(ExcludeTrailingPathDelimiter(sPath)); end; class function TAndroidHelperEx.GetTargetSdkVersion: Integer; var LApplicationInfo: JApplicationInfo; begin try LApplicationInfo := Context.getPackageManager.getApplicationInfo(Context.getPackageName, 0); if LApplicationInfo <> nil then Result := LApplicationInfo.targetSdkVersion else Result := -1; except Result := -1; end; end; class function TAndroidHelperEx.HasAssocApp(const Intent: JIntent): Boolean; var LList: JList; begin Result := False; if Intent = nil then Exit; Result := Intent.resolveActivityInfo(Activity.getPackageManager(), TJPackageManager.JavaClass.MATCH_DEFAULT_ONLY) <> nil; // Android 6+ APP LINK closed Will cause list is null. // Activity with // 被第三方软件套用的时候,比如应用隐藏大师,会导致这里size不正确 // LList := Activity.getPackageManager.queryIntentActivities(Intent, // TJPackageManager.JavaClass.MATCH_DEFAULT_ONLY); // Result := (LList = nil) or (LList.size > 0); end; class function TAndroidHelperEx.HasAssocApp(const URI: string): Boolean; var Intent: JIntent; begin Result := False; if URI = '' then Exit; Intent := TJIntent.Create; Intent.setData(UriParse(URI)); Intent.setAction(StringToJString('android.intent.action.VIEW')); Result := HasAssocApp(Intent); end; class function TAndroidHelperEx.InstallPackage(const AFileName, AAuthority: string): Boolean; var LIntent: JIntent; begin Result := False; if Trim(AFileName) = '' then Exit; LIntent := TJIntent.Create; if CheckBuildAndTarget(OREO) then LIntent.setAction(TJIntent.JavaClass.ACTION_INSTALL_PACKAGE) else LIntent.setAction(TJIntent.JavaClass.ACTION_VIEW); // 没有这个也可以安装成功,但是安装成功后的成功页面,也就是,完成/打开 会无法显示 LIntent.addFlags(TJIntent.JavaClass.FLAG_ACTIVITY_NEW_TASK); LIntent.setDataAndType(SharedUriFromFile(AFileName, AAuthority), StringToJString('application/vnd.android.package-archive')); LIntent.addFlags(TJIntent.JavaClass.FLAG_GRANT_READ_URI_PERMISSION); Result := StartActivity(LIntent); end; class function TAndroidHelperEx.IsActivityForeground: Boolean; var LService: JObject; LRunningApps: JList; LAppInfo: JActivityManager_RunningAppProcessInfo; I: Integer; begin Result := False; LService := Context.getSystemService(TJContext.JavaClass.ACTIVITY_SERVICE); LRunningApps := TJActivityManager.Wrap(TAndroidHelper.JObjectToID(LService)).getRunningAppProcesses; for I := 0 to LRunningApps.size - 1 do begin LAppInfo := TJActivityManager_RunningAppProcessInfo.Wrap(TAndroidHelper.JObjectToID(LRunningApps.get(I))); if LAppInfo.importance = 100 then begin if LAppInfo.importanceReasonComponent <> nil then begin if LAppInfo.importanceReasonComponent.getPackageName.equals(Context.getPackageName) then Exit(True); end else if LRunningApps.size = 1 then Exit(True); end; end; end; class function TAndroidHelperEx.IsAppInstalled(const APackage: string): Boolean; begin Result := False; try //只有异常是可靠的,返回值判定不对 Result := Context.getPackageManager.getPackageInfo(StringToJString(APackage), TJPackageManager.JavaClass.GET_ACTIVITIES) = nil; Result := True; except end; end; class function TAndroidHelperEx.KeepScreen(AOn: Boolean): Boolean; begin CallInUIThreadAndWaitFinishing( procedure begin if AOn then SharedActivity.getWindow.addFlags( TJWindowManager_LayoutParams.JavaClass.FLAG_KEEP_SCREEN_ON) else SharedActivity.getWindow.clearFlags( TJWindowManager_LayoutParams.JavaClass.FLAG_KEEP_SCREEN_ON); end); end; class procedure TAndroidHelperEx.SendAppToBack; begin //SharedActivityManager.moveTaskToBack(Activity.getTaskId, TJIntent.JavaClass.FLAG_ACTIVITY_NEW_TASK); Activity.moveTaskToBack(True); end; class function TAndroidHelperEx.StartActivity(Intent: JIntent; const Code: Integer): Boolean; begin Result := False; if Code = -1 then Activity.startActivity(Intent) else Activity.startActivityForResult(Intent, Code); Result := True; end; class function TAndroidHelperEx.UriParse(const S: JString): Jnet_Uri; begin Result := TJnet_Uri.JavaClass.parse(S); end; class function TAndroidHelperEx.UriParse(const S: string): Jnet_Uri; begin Result := UriParse(StringToJString(S)); end; class function TAndroidHelperEx.UriFromFile(const AFileName: string): Jnet_Uri; begin Result := UriFromFile(TJFile.JavaClass.init(StringToJString(AFileName))); end; class function TAndroidHelperEx.UriFromFile(const AFile: JFile): Jnet_Uri; begin Result := TJnet_uri.JavaClass.fromFile(AFile); end; class function TAndroidHelperEx.SharedUriFromFile(const AFile: JFile; const AAuthority: string): Jnet_Uri; var LAuthority: JString; begin if CheckBuildAndTarget(NOUGAT) then begin if AAuthority <> '' then LAuthority := StringToJString(AAuthority) else LAuthority := Context.getApplicationContext.getPackageName.concat(StringToJString('.fileprovider')); Result := TJFileProvider.JavaClass.getUriForFile(Context, LAuthority, AFile); end else Result := TJnet_uri.JavaClass.fromFile(AFile); end; class function TAndroidHelperEx.SharedUriFromFile(const AFileName, AAuthority: string): Jnet_Uri; begin Result := SharedUriFromFile(TJFile.JavaClass.init(StringToJString(AFileName)), AAuthority); end; end.