root/java/org/gnu/emacs/EmacsDialog.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. onClick
  2. onClick
  3. createDialog
  4. addButton
  5. toAlertDialog
  6. display1
  7. display
  8. onDismiss

     1 /* Communication module for Android terminals.  -*- c-file-style: "GNU" -*-
     2 
     3 Copyright (C) 2023 Free Software Foundation, Inc.
     4 
     5 This file is part of GNU Emacs.
     6 
     7 GNU Emacs is free software: you can redistribute it and/or modify
     8 it under the terms of the GNU General Public License as published by
     9 the Free Software Foundation, either version 3 of the License, or (at
    10 your option) any later version.
    11 
    12 GNU Emacs is distributed in the hope that it will be useful,
    13 but WITHOUT ANY WARRANTY; without even the implied warranty of
    14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    15 GNU General Public License for more details.
    16 
    17 You should have received a copy of the GNU General Public License
    18 along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */
    19 
    20 package org.gnu.emacs;
    21 
    22 import java.util.List;
    23 import java.util.ArrayList;
    24 
    25 import android.app.AlertDialog;
    26 
    27 import android.content.Context;
    28 import android.content.DialogInterface;
    29 
    30 import android.content.res.Resources.NotFoundException;
    31 import android.content.res.Resources.Theme;
    32 import android.content.res.TypedArray;
    33 
    34 import android.os.Build;
    35 
    36 import android.provider.Settings;
    37 
    38 import android.util.Log;
    39 
    40 import android.widget.Button;
    41 import android.widget.LinearLayout;
    42 import android.widget.FrameLayout;
    43 
    44 import android.view.View;
    45 import android.view.ViewGroup;
    46 import android.view.Window;
    47 import android.view.WindowManager;
    48 
    49 /* Toolkit dialog implementation.  This object is built from JNI and
    50    describes a single alert dialog.  Then, `inflate' turns it into
    51    AlertDialog.  */
    52 
    53 public final class EmacsDialog implements DialogInterface.OnDismissListener
    54 {
    55   private static final String TAG = "EmacsDialog";
    56 
    57   /* List of buttons in this dialog.  */
    58   private List<EmacsButton> buttons;
    59 
    60   /* Dialog title.  */
    61   private String title;
    62 
    63   /* Dialog text.  */
    64   private String text;
    65 
    66   /* Whether or not a selection has already been made.  */
    67   private boolean wasButtonClicked;
    68 
    69   /* Dialog to dismiss after click.  */
    70   private AlertDialog dismissDialog;
    71 
    72   /* The menu serial associated with this dialog box.  */
    73   private int menuEventSerial;
    74 
    75   private final class EmacsButton implements View.OnClickListener,
    76                                   DialogInterface.OnClickListener
    77   {
    78     /* Name of this button.  */
    79     public String name;
    80 
    81     /* ID of this button.  */
    82     public int id;
    83 
    84     /* Whether or not the button is enabled.  */
    85     public boolean enabled;
    86 
    87     @Override
    88     public void
    89     onClick (View view)
    90     {
    91       Log.d (TAG, "onClicked " + this);
    92 
    93       wasButtonClicked = true;
    94       EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
    95       dismissDialog.dismiss ();
    96     }
    97 
    98     @Override
    99     public void
   100     onClick (DialogInterface dialog, int which)
   101     {
   102       Log.d (TAG, "onClicked " + this);
   103 
   104       wasButtonClicked = true;
   105       EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
   106     }
   107   };
   108 
   109   /* Create a popup dialog with the title TITLE and the text TEXT.
   110      TITLE may be NULL.  MENUEVENTSERIAL is a number which will
   111      identify this popup dialog inside events it sends.  */
   112 
   113   public static EmacsDialog
   114   createDialog (String title, String text, int menuEventSerial)
   115   {
   116     EmacsDialog dialog;
   117 
   118     dialog = new EmacsDialog ();
   119     dialog.buttons = new ArrayList<EmacsButton> ();
   120     dialog.title = title;
   121     dialog.text = text;
   122     dialog.menuEventSerial = menuEventSerial;
   123 
   124     return dialog;
   125   }
   126 
   127   /* Add a button named NAME, with the identifier ID.  If DISABLE,
   128      disable the button.  */
   129 
   130   public void
   131   addButton (String name, int id, boolean disable)
   132   {
   133     EmacsButton button;
   134 
   135     button = new EmacsButton ();
   136     button.name = name;
   137     button.id = id;
   138     button.enabled = !disable;
   139     buttons.add (button);
   140   }
   141 
   142   /* Turn this dialog into an AlertDialog for the specified
   143      CONTEXT.
   144 
   145      Upon a button being selected, the dialog will send an
   146      ANDROID_CONTEXT_MENU event with the id of that button.
   147 
   148      Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event
   149      will be sent with an id of 0.  */
   150 
   151   public AlertDialog
   152   toAlertDialog (Context context)
   153   {
   154     AlertDialog dialog;
   155     int size, styleId, flag;
   156     int[] attrs;
   157     EmacsButton button;
   158     EmacsDialogButtonLayout layout;
   159     Button buttonView;
   160     ViewGroup.LayoutParams layoutParams;
   161     Theme theme;
   162     TypedArray attributes;
   163     Window window;
   164 
   165     size = buttons.size ();
   166     styleId = -1;
   167 
   168     if (size <= 3)
   169       {
   170         dialog = new AlertDialog.Builder (context).create ();
   171         dialog.setMessage (text);
   172         dialog.setCancelable (true);
   173         dialog.setOnDismissListener (this);
   174 
   175         if (title != null)
   176           dialog.setTitle (title);
   177 
   178         /* There are less than 4 buttons.  Add the buttons the way
   179            Android intends them to be added.  */
   180 
   181         if (size >= 1)
   182           {
   183             button = buttons.get (0);
   184             dialog.setButton (DialogInterface.BUTTON_POSITIVE,
   185                               button.name, button);
   186           }
   187 
   188         if (size >= 2)
   189           {
   190             button = buttons.get (1);
   191             dialog.setButton (DialogInterface.BUTTON_NEGATIVE,
   192                               button.name, button);
   193           }
   194 
   195         if (size >= 3)
   196           {
   197             button = buttons.get (2);
   198             dialog.setButton (DialogInterface.BUTTON_NEUTRAL,
   199                               button.name, button);
   200           }
   201       }
   202     else
   203       {
   204         /* There are more than 3 buttons.  Add them all to a special
   205            container widget that handles wrapping.  First, create the
   206            layout.  */
   207 
   208         layout = new EmacsDialogButtonLayout (context);
   209         layoutParams
   210           = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT,
   211                                           ViewGroup.LayoutParams.WRAP_CONTENT);
   212         layout.setLayoutParams (layoutParams);
   213 
   214         /* Add that layout to the dialog's custom view.
   215 
   216            android.R.id.custom is documented to work.  But looking it
   217            up returns NULL, so setView must be used instead.  */
   218 
   219         dialog = new AlertDialog.Builder (context).setView (layout).create ();
   220         dialog.setMessage (text);
   221         dialog.setCancelable (true);
   222         dialog.setOnDismissListener (this);
   223 
   224         if (title != null)
   225           dialog.setTitle (title);
   226 
   227         /* Now that the dialog has been created, set the style of each
   228            custom button to match the usual dialog buttons found on
   229            Android 5 and later.  */
   230 
   231         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
   232           {
   233             /* Obtain the Theme associated with the dialog.  */
   234             theme = dialog.getContext ().getTheme ();
   235 
   236             /* Resolve the dialog button style.  */
   237             attrs
   238               = new int [] { android.R.attr.buttonBarNeutralButtonStyle, };
   239 
   240             try
   241               {
   242                 attributes = theme.obtainStyledAttributes (attrs);
   243 
   244                 /* Look for the style ID.  Default to -1 if it could
   245                    not be found.  */
   246                 styleId = attributes.getResourceId (0, -1);
   247 
   248                 /* Now clean up the TypedAttributes object.  */
   249                 attributes.recycle ();
   250               }
   251             catch (NotFoundException e)
   252               {
   253                 /* Nothing to do here.  */
   254               }
   255           }
   256 
   257         /* Create each button and add it to the layout.  Set the style
   258            if necessary.  */
   259 
   260         for (EmacsButton emacsButton : buttons)
   261           {
   262             if (styleId == -1)
   263               /* No specific style... */
   264               buttonView = new Button (context);
   265             else
   266               /* Use the given styleId.  */
   267               buttonView = new Button (context, null, 0, styleId);
   268 
   269             /* Set the text and on click handler.  */
   270             buttonView.setText (emacsButton.name);
   271             buttonView.setOnClickListener (emacsButton);
   272             buttonView.setEnabled (emacsButton.enabled);
   273             layout.addView (buttonView);
   274           }
   275       }
   276 
   277     return dialog;
   278   }
   279 
   280   /* Internal helper for display run on the main thread.  */
   281 
   282   @SuppressWarnings("deprecation")
   283   private boolean
   284   display1 ()
   285   {
   286     Context context;
   287     int size, type;
   288     Button buttonView;
   289     EmacsButton button;
   290     AlertDialog dialog;
   291     Window window;
   292 
   293     if (EmacsActivity.focusedActivities.isEmpty ())
   294       {
   295         /* If focusedActivities is empty then this dialog may have
   296            been displayed immediately after another popup dialog was
   297            dismissed.  Or Emacs might legitimately be in the
   298            background, possibly displaying this popup in response to
   299            an Emacsclient request.  Try the service context if it will
   300            work, then any focused EmacsOpenActivity, and finally the
   301            last EmacsActivity to be focused.  */
   302 
   303         Log.d (TAG, "display1: no focused activities...");
   304         Log.d (TAG, ("display1: EmacsOpenActivity.currentActivity: "
   305                      + EmacsOpenActivity.currentActivity));
   306 
   307         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
   308             || Settings.canDrawOverlays (EmacsService.SERVICE))
   309           context = EmacsService.SERVICE;
   310         else if (EmacsOpenActivity.currentActivity != null)
   311           context = EmacsOpenActivity.currentActivity;
   312         else
   313           context = EmacsActivity.lastFocusedActivity;
   314 
   315         if (context == null)
   316           return false;
   317       }
   318     else
   319       /* Display using the activity context when Emacs is in the
   320          foreground, as this allows the dialog to be dismissed more
   321          consistently.  */
   322       context = EmacsActivity.focusedActivities.get (0);
   323 
   324     Log.d (TAG, "display1: using context " + context);
   325 
   326     dialog = dismissDialog = toAlertDialog (context);
   327 
   328     try
   329       {
   330         if (context == EmacsService.SERVICE)
   331           {
   332             /* Apply the system alert window type to make sure this
   333                dialog can be displayed.  */
   334 
   335             window = dialog.getWindow ();
   336             type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
   337                     ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
   338                     : WindowManager.LayoutParams.TYPE_PHONE);
   339             window.setType (type);
   340           }
   341 
   342         dismissDialog.show ();
   343       }
   344     catch (Exception exception)
   345       {
   346         /* This can happen when the system decides Emacs is not in the
   347            foreground any longer.  */
   348         return false;
   349       }
   350 
   351     /* If there are less than four buttons, then they must be
   352        individually enabled or disabled after the dialog is
   353        displayed.  */
   354     size = buttons.size ();
   355 
   356     if (size <= 3)
   357       {
   358         if (size >= 1)
   359           {
   360             button = buttons.get (0);
   361             buttonView
   362               = dialog.getButton (DialogInterface.BUTTON_POSITIVE);
   363             buttonView.setEnabled (button.enabled);
   364           }
   365 
   366         if (size >= 2)
   367           {
   368             button = buttons.get (1);
   369             buttonView
   370               = dialog.getButton (DialogInterface.BUTTON_NEGATIVE);
   371             buttonView.setEnabled (button.enabled);
   372           }
   373 
   374         if (size >= 3)
   375           {
   376             button = buttons.get (2);
   377             buttonView
   378               = dialog.getButton (DialogInterface.BUTTON_NEUTRAL);
   379             buttonView.setEnabled (button.enabled);
   380           }
   381       }
   382 
   383     return true;
   384   }
   385 
   386   /* Display this dialog for a suitable activity.
   387      Value is false if the dialog could not be displayed,
   388      and true otherwise.  */
   389 
   390   public boolean
   391   display ()
   392   {
   393     Runnable runnable;
   394     final EmacsHolder<Boolean> rc;
   395 
   396     rc = new EmacsHolder<Boolean> ();
   397     runnable = new Runnable () {
   398         @Override
   399         public void
   400         run ()
   401         {
   402           synchronized (this)
   403             {
   404               rc.thing = display1 ();
   405               notify ();
   406             }
   407         }
   408       };
   409 
   410     EmacsService.syncRunnable (runnable);
   411     return rc.thing;
   412   }
   413 
   414 
   415 
   416   @Override
   417   public void
   418   onDismiss (DialogInterface dialog)
   419   {
   420     Log.d (TAG, "onDismiss: " + this);
   421 
   422     if (wasButtonClicked)
   423       return;
   424 
   425     EmacsNative.sendContextMenu ((short) 0, 0, menuEventSerial);
   426   }
   427 };

/* [<][>][^][v][top][bottom][index][help] */