{*******************************************************}
{ }
{ 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.