Abdul Khader
Abdul Khader

Reputation: 89

Threads on Android widget

I'm trying to make a widget clock.

On my MainActivity, the clock is displayed perfectly using runOnUiThread. Since the widget doesn't extend "activity" I cannot runOnUiThread.
But when I try to use Thread with the widget I get errors (or) a blank widget.

public class NewAppWidget extends AppWidgetProvider {
static Context cont;
static AppWidgetManager awm;
static int awid;
static Thread  t;
static RemoteViews views;
static String timeText;
static String dateText;
public static Bitmap BuildUpdate(String txttime, int size,Context context){
    Paint paint= new Paint();
    paint.setTextSize(size);
    Typeface 
    custTface=Typeface.createFromAsset(context.getAssets(),"fonts/Lato-Regular.ttf");
    paint.setTypeface(custTface);
    paint.setColor(Color.WHITE);
    paint.setTextAlign(Paint.Align.LEFT);
    paint.setSubpixelText(true);
    paint.setAntiAlias(true);
    // - in the  next line is highly important.
    float baseline  = -paint.ascent();
    int width =(int) paint.measureText(txttime+0.5f);
    int height= (int) (baseline+paint.descent()+0.5f);
    Bitmap image = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
    Canvas canvas=new Canvas(image);
    canvas.drawText(txttime,0,baseline,paint);
    return image;
}

static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                            int appWidgetId) {
     views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
     cont=context;
     awm=appWidgetManager;
     awid=appWidgetId;
      t = new Thread() {
         @Override
         public void run() {
             while (!isInterrupted()){
                 try {
                     t.sleep(1000);
                     new Thread(new Runnable(){
                         @Override
                         public void run() {
                             long date = System.currentTimeMillis();
                             SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
                             SimpleDateFormat sdf2 = new SimpleDateFormat("hh-mm-ss a");
                             timeText = sdf2.format(date);
                             dateText = sdf.format(date);
                             views.setImageViewBitmap(R.id.txtTime, BuildUpdate(timeText, 100, cont));
                             views.setImageViewBitmap(R.id.txtDate, BuildUpdate(dateText, 25, cont));
                             awm.updateAppWidget(awid, views);
                         }
                     }); 

                 }catch (InterruptedException e) {
                 }
             }
         }
     };
     t.start();
}

This produces a blank widget.

I'm not asking for the code, just help me identify what is my mistake here & How should I approach that?

Upvotes: 2

Views: 1510

Answers (2)

basileus
basileus

Reputation: 445

I know that this is an old problem and it has been resolved for author of this thread.

However for future readers I would like to modify answer given by #oaskamay (https://stackoverflow.com/a/49520145/13755907).

  1. The second approach, using Handler, worked for the clock problem. This is correct and reasonable in this case.

  2. The first approach is of no use. As this is AppWidgetProvider class called by the system (when new Widget is created on the screen) only default constructor is used, thus there is no way of providing Activity during creating a new instance

  3. However if anyone still needs to use non-UI Thread in the widget, like for fetching data for internet, which Android prohibits to be run on any UI Thread, then solution to above would be to use... Handler but within Thread. This way you will return with your data from non-UI Worker to UI based Handler. I marked new pieces of the code with // New line comment. I also changed the method from static to standard (I no not understand the purpose of static methods here).

     public class NewAppWidget extends AppWidgetProvider {
     private Context cont;
     private AppWidgetManager awm;
     private int awid;
     private Thread  t;
     private RemoteViews views;
     private String timeText;
     private String dateText;
    
     // New line
     private Handler handler = new Handler(Looper.getMainLooper());
    
     public Bitmap BuildUpdate(String txttime, int size,Context context){
         Paint paint= new Paint();
         paint.setTextSize(size);
         Typeface
                 custTface=Typeface.createFromAsset(context.getAssets(),"fonts/Lato-Regular.ttf");
         paint.setTypeface(custTface);
         paint.setColor(Color.WHITE);
         paint.setTextAlign(Paint.Align.LEFT);
         paint.setSubpixelText(true);
         paint.setAntiAlias(true);
         // - in the  next line is highly important.
         float baseline  = -paint.ascent();
         int width =(int) paint.measureText(txttime+0.5f);
         int height= (int) (baseline+paint.descent()+0.5f);
         Bitmap image = Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
         Canvas canvas=new Canvas(image);
         canvas.drawText(txttime,0,baseline,paint);
         return image;
     }
    
     void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                 int appWidgetId) {
         views = new RemoteViews(context.getPackageName(), R.layout.new_app_widget);
         cont=context;
         awm=appWidgetManager;
         awid=appWidgetId;
         t = new Thread() {
             @Override
             public void run() {
                 while (!isInterrupted()){
                     try {
                         t.sleep(1000);
                         new Thread(new Runnable(){
                             @Override
                             public void run() {
                                 long date = System.currentTimeMillis();
                                 SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
                                 SimpleDateFormat sdf2 = new SimpleDateFormat("hh-mm-ss a");
                                 timeText = sdf2.format(date);
                                 dateText = sdf.format(date);
    
                                 // New line
                                 handler.post(new Runnable() {
                                     @Override
                                     public void run() {                                            views.setImageViewBitmap(R.id.txtTime, BuildUpdate(timeText, 100, cont));
                                         views.setImageViewBitmap(R.id.txtDate, BuildUpdate(dateText, 25, cont));
                                         awm.updateAppWidget(awid, views);
                                     }
                                 });
                             }
                         });
    
                     }catch (InterruptedException e) {
                     }
                 }
             }
         };
         t.start();
     }
    

    }

Upvotes: 0

oaskamay
oaskamay

Reputation: 274

Your Thread approach does not work because Android disallows Threads who did not create the View hierarchy from touching those Views. Like most cases, your View that you're trying to update from your worker Thread was most likely inflated and instantiated on the UI thread as part of your Activity's creation process by Android.

I think you have two options here:

  1. Always pass an Activity as your Context argument in your NewAppWidget constructor.

    Then you can either safely cast the Context object to an Activity and call Activity.runOnUiThread(Runnable), or change the Context field to be an Activity to avoid having to cast every time.

  2. Use a Handler.

    Instantiate your Handler as follows. This will create a Handler object that is linked to the UI thread (also called the "main" thread).

    Handler handler = new Handler(Looper.getMainLooper());

    You can then use the Handler to post Runnables which will then be run on the UI Thread from your worker Thread. You'll be able to update Views from your worker Thread this way:

    handler.post(new Runnable() { ... });

I recommend the second approach as it is cleaner and you won't always need a reference to an Activity this way.

Keep in mind with the second approach, in your Runnable you'll want to use a WeakReference to your Activity or View that you want to update to prevent memory leaks. Your worker Thread may still be running even when your Activity or View is (supposed to be) garbage collected. Using WeakReference allows the garbage collector to collect your Activity / View even when your worker Thread is still running.

Upvotes: 1

Related Questions