feat(android): mark the clipboard sensitive, and lock after 3 min idle
Since Android 13 the system shows a preview of whatever was just copied, at the bottom of the screen. For a password that is precisely what we are trying to avoid: it appears in clear, outside the app, for several seconds. The flag that suppresses it is a ClipDescription extra, which IFMXClipboardService does not expose - hence going straight to ClipboardManager. Note Jcontent_ClipboardManager, not JClipboardManager: the latter is the deprecated android.text one, which knows only bare text and has no ClipData to carry an extra. Backgrounding already locks; the idle timer covers the phone left face-up on a table with the app open. It samples timestamps every 15 s rather than counting seconds - a wake-up per second would cost battery for no precision that matters. Attachments dropped from the plan: this vault has none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
unit PMA.Clipboard;
|
||||
{
|
||||
Copie dans le presse-papiers, en marquant le contenu SENSIBLE.
|
||||
|
||||
POURQUOI PAS IFMXClipboardService : depuis Android 13, le systeme affiche un
|
||||
apercu de ce qui vient d'etre copie, en bas de l'ecran. Pour un mot de passe,
|
||||
c'est exactement ce qu'on cherche a eviter - il s'affiche en clair, hors de
|
||||
l'application, et reste visible quelques secondes.
|
||||
|
||||
Le drapeau qui supprime cet apercu est un extra du ClipDescription, que le
|
||||
service FMX n'expose pas. D'ou ce passage direct par ClipboardManager.
|
||||
|
||||
Cela ne remplace pas l'effacement differe : le drapeau cache l'apercu, il ne
|
||||
vide pas le presse-papiers.
|
||||
}
|
||||
|
||||
interface
|
||||
|
||||
/// Copie ATexte. Retourne False si la plateforme n'a rien fourni - l'appelant
|
||||
/// peut alors retomber sur le service FMX.
|
||||
function CopySensitive(const AText: string): Boolean;
|
||||
|
||||
/// Contenu courant, ou chaine vide. Sert a ne vider le presse-papiers que
|
||||
/// s'il contient toujours ce qu'on y a mis.
|
||||
function CurrentClipboard: string;
|
||||
|
||||
/// Vide le presse-papiers.
|
||||
procedure ClearClipboard;
|
||||
|
||||
implementation
|
||||
|
||||
{$IF DEFINED(ANDROID)}
|
||||
uses
|
||||
System.SysUtils, Androidapi.Helpers, Androidapi.JNI.JavaTypes,
|
||||
Androidapi.JNI.GraphicsContentViewText, Androidapi.JNI.Os, Androidapi.JNIBridge;
|
||||
|
||||
const
|
||||
// android.content.ClipDescription.EXTRA_IS_SENSITIVE, ecrit en dur : la
|
||||
// constante n'existe qu'a partir de l'API 33 dans les en-tetes, alors que la
|
||||
// valeur est reconnue par le systeme des qu'il sait la lire, et ignoree
|
||||
// ailleurs. Un extra inconnu ne derange personne.
|
||||
EXTRA_IS_SENSITIVE = 'android.content.extra.IS_SENSITIVE';
|
||||
|
||||
/// Jcontent_ClipboardManager et NON JClipboardManager : ce dernier est
|
||||
/// l'android.text.ClipboardManager deprecie, qui ne connait que du texte nu et
|
||||
/// aucun ClipData - donc aucun extra ou poser le drapeau.
|
||||
function Manager: Jcontent_ClipboardManager;
|
||||
var
|
||||
LObj: JObject;
|
||||
begin
|
||||
LObj := TAndroidHelper.Context.getSystemService(
|
||||
TJContext.JavaClass.CLIPBOARD_SERVICE);
|
||||
if LObj = nil then Exit(nil);
|
||||
Result := TJcontent_ClipboardManager.Wrap((LObj as ILocalObject).GetObjectID);
|
||||
end;
|
||||
|
||||
function CopySensitive(const AText: string): Boolean;
|
||||
var
|
||||
LMgr: Jcontent_ClipboardManager;
|
||||
LClip: JClipData;
|
||||
LExtras: JPersistableBundle;
|
||||
begin
|
||||
Result := False;
|
||||
try
|
||||
LMgr := Manager;
|
||||
if LMgr = nil then Exit;
|
||||
// L'etiquette est visible du systeme : elle ne doit rien decrire du
|
||||
// contenu, seulement son origine.
|
||||
// Les DEUX arguments sont des CharSequence : passer un JString au premier
|
||||
// compile ailleurs mais pas ici.
|
||||
LClip := TJClipData.JavaClass.newPlainText(StrToJCharSequence('coffre'),
|
||||
StrToJCharSequence(AText));
|
||||
LExtras := TJPersistableBundle.JavaClass.init;
|
||||
LExtras.putBoolean(StringToJString(EXTRA_IS_SENSITIVE), True);
|
||||
LClip.getDescription.setExtras(LExtras);
|
||||
LMgr.setPrimaryClip(LClip);
|
||||
Result := True;
|
||||
except
|
||||
Result := False; // l'appelant retombera sur le service FMX
|
||||
end;
|
||||
end;
|
||||
|
||||
function CurrentClipboard: string;
|
||||
var
|
||||
LMgr: Jcontent_ClipboardManager;
|
||||
LClip: JClipData;
|
||||
begin
|
||||
Result := '';
|
||||
try
|
||||
LMgr := Manager;
|
||||
if (LMgr = nil) or not LMgr.hasPrimaryClip then Exit;
|
||||
LClip := LMgr.getPrimaryClip;
|
||||
if (LClip = nil) or (LClip.getItemCount < 1) then Exit;
|
||||
Result := JCharSequenceToStr(LClip.getItemAt(0).getText);
|
||||
except
|
||||
Result := '';
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure ClearClipboard;
|
||||
begin
|
||||
// Un contenu vide plutot qu'un appel a clearPrimaryClip, absent avant l'API
|
||||
// 28 : le resultat visible est le meme.
|
||||
CopySensitive('');
|
||||
end;
|
||||
{$ELSE}
|
||||
uses
|
||||
System.SysUtils, System.Rtti, FMX.Platform;
|
||||
|
||||
function CopySensitive(const AText: string): Boolean;
|
||||
var
|
||||
LClip: IFMXClipboardService;
|
||||
begin
|
||||
// Hors Android il n'y a pas d'apercu systeme a supprimer : le service
|
||||
// standard suffit.
|
||||
Result := TPlatformServices.Current.SupportsPlatformService(
|
||||
IFMXClipboardService, LClip);
|
||||
if Result then LClip.SetClipboard(AText);
|
||||
end;
|
||||
|
||||
function CurrentClipboard: string;
|
||||
var
|
||||
LClip: IFMXClipboardService;
|
||||
begin
|
||||
Result := '';
|
||||
if TPlatformServices.Current.SupportsPlatformService(IFMXClipboardService, LClip) then
|
||||
Result := LClip.GetClipboard.ToString;
|
||||
end;
|
||||
|
||||
procedure ClearClipboard;
|
||||
begin
|
||||
CopySensitive('');
|
||||
end;
|
||||
{$ENDIF}
|
||||
|
||||
end.
|
||||
Reference in New Issue
Block a user